diff --git a/.travis.yml b/.travis.yml index d451ec9..482a12d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,10 @@ +dist: xenial language: ruby rvm: - - 1.9.3 - - 2.0.0 - - 2.1.0 + - 2.3.8 + - 2.4.5 + - 2.5.3 + - 2.6.3 - jruby-19mode - rbx-19mode - ruby-head @@ -13,4 +15,6 @@ matrix: - rvm: ruby-head notifications: irc: "irc.freenode.org#adhearsion" -sudo: false +before_install: + - rvm list + - sudo apt-get install libpcap-dev -y diff --git a/CHANGELOG.md b/CHANGELOG.md index c317dd0..2c2e391 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ # develop + * Drop support for Ruby < 2.3.0 - Too many gem dependences require Ruby 2.3, so go along with it rather than fight it + * Require PacketFu >= 1.1.13, 1.1.12 was just broken # [0.7.2](https://github.com/mojolingo/sippy_cup/compare/v0.7.1...v0.7.2) * Bugfix: Logical destination for dialog formation belongs in request line diff --git a/README.markdown b/README.markdown index aee6302..2012e20 100644 --- a/README.markdown +++ b/README.markdown @@ -33,13 +33,13 @@ Sippy Cup is a tool to generate [SIPp](http://sipp.sourceforge.net/) load test p SippyCup relies on the following to generate scenarios and the associated media PCAP files: -* Ruby 1.9.3 or later (2.1.2 recommended) +* Ruby 2.3.0 or later * [SIPp](http://sipp.sourceforge.net/) latest master branch - Download from https://github.com/sipp/sipp - NOTE: Version SIPp version 3.4 may work, but will be missing certain new Sippy Cup features, such as rate scaling * "root" user access via sudo: needed to run SIPp so it can bind to raw network sockets ## Installation -If you do not have Ruby 2.1.2 available (check using `ruby --version`), we recommend installing Ruby with [RVM](http://rvm.io) +If you do not have Ruby 2.3.3 available (check using `ruby --version`), we recommend installing Ruby with [RVM](http://rvm.io) ### Install via gem (production) @@ -139,7 +139,7 @@ The above code can be executed as a standalone Ruby script and the resulting sce Each command below can take [SIPp attributes](http://sipp.sourceforge.net/doc/reference.html) as optional arguments. For a full list of available steps with arguments explained, see the [API documentation](http://rubydoc.info/gems/sippy_cup/SippyCup/Scenario). -* `sleep ` Wait a specified number of seconds +#### Initiation Commands * `invite` Send a SIP INVITE to the specified target * `receive_invite` Wait for an INVITE to be received * `register [password]` Register the specified user to the target with an optional password @@ -154,9 +154,21 @@ Each command below can take [SIPp attributes](http://sipp.sourceforge.net/doc/re * `wait_for_answer` Convenient shortcut for `receive_trying; receive_ringing; receive_progress; receive_answer`, with all but the `answer` marked as optional * `ack_answer` Send an `ACK` in response to a `200 OK` * `receive_ack` Expect to receive an `ACK` + +#### Interaction Commands * `send_digits ` Send a DTMF string. May send one or many digits, including `0-9`, `*`, `#`, and `A-D` +* `sleep ` Wait a specified number of seconds * `receive_ok` Expect to receive a `200 OK` * `receive_message [regex]` Expect to receive a SIP MESSAGE, optionally matching a regex + +#### Transfer Commands +* `receive_refer` Expect to receive a `REFER` from the target +* `ack_refer` Send an ACK in response to a `REFER` +* `notify_refer_ringing` Send a `NOTIFY` with a sipfrag of `180 Ringing` +* `notify_refer_ok` Send a `NOTIFY` with a sipfrag of `200 OK` +* `wait_for_refer` Convenient shortcut for `receive_refer; ack_refer; notify_refer_ringing; receive_answer; notify_refer_ok; receive_answer` + +#### Teardown Commands * `send_bye` Send a `BYE` (hangup request) * `receive_bye` Expect to receive a `BYE` from the target * `ack_bye` Send a `200 OK` response to a `BYE` @@ -273,7 +285,7 @@ For more information on possible attributes, visit the [SIPp Documentation](http ## Credits -Copyright (C) 2013-2014 [Mojo Lingo LLC](https://mojolingo.com) +Copyright (C) 2013-2015 [Mojo Lingo LLC](https://mojolingo.com) Sippy Cup is released under the [MIT license](http://opensource.org/licenses/MIT). Please see the [LICENSE](https://github.com/bklang/sippy_cup/blob/master/LICENSE) file for details. diff --git a/lib/sippy_cup/media.rb b/lib/sippy_cup/media.rb index acae59e..3789d5b 100644 --- a/lib/sippy_cup/media.rb +++ b/lib/sippy_cup/media.rb @@ -67,19 +67,31 @@ def compile! # value is the DTMF digit to send # append that RFC2833 digit # Assume 0.25 second duration for now - count = 250 / DTMFPayload::PTIME + count = (250 / DTMFPayload::PTIME) + 1 count.times do |i| packet = new_packet + elapsed += DTMFPayload::PTIME dtmf_frame = DTMFPayload.new value dtmf_frame.rtp_marker = 1 if i == 0 + dtmf_frame.index = i dtmf_frame.rtp_timestamp = timestamp # Is this correct? This is what Blink does... #dtmf_frame.rtp_timestamp = timestamp += dtmf_frame.timestamp_interval 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 = ((count-1) == i) # Last packet? packet.headers.last.body = dtmf_frame.to_bytes packet.recalc @pcap_file.body << get_pcap_packet(packet, next_ts(start_time, elapsed)) + + # if the end-of-event, add some redundant packets in case of loss + # during transmission + if dtmf_frame.end_of_event + 2.times do + dtmf_frame.rtp_sequence_num = sequence_number += 1 + packet.recalc + @pcap_file.body << get_pcap_packet(packet, next_ts(start_time, elapsed)) + end + end end # Now bump up the timestamp to cover the gap timestamp += count * DTMFPayload::TIMESTAMP_INTERVAL diff --git a/lib/sippy_cup/media/dtmf_payload.rb b/lib/sippy_cup/media/dtmf_payload.rb index 717dfc0..de62bdf 100644 --- a/lib/sippy_cup/media/dtmf_payload.rb +++ b/lib/sippy_cup/media/dtmf_payload.rb @@ -9,7 +9,7 @@ class DTMFPayload < RTPPayload TIMESTAMP_INTERVAL = 160 END_OF_EVENT = 1 << 7 DTMF = %w{0 1 2 3 4 5 6 7 8 9 * # A B C D}.freeze - attr_accessor :ptime + attr_accessor :ptime, :index def initialize(digit, opts = {}) super RTP_PAYLOAD_ID @@ -39,7 +39,7 @@ def volume(value) end def end_of_event - @flags & END_OF_EVENT + 0 < (@flags & END_OF_EVENT) end def media @@ -47,7 +47,7 @@ def media end def timestamp_interval - TIMESTAMP_INTERVAL + TIMESTAMP_INTERVAL * (index.to_i+1) end end end diff --git a/lib/sippy_cup/runner.rb b/lib/sippy_cup/runner.rb index b54c6d7..7ffeaa4 100644 --- a/lib/sippy_cup/runner.rb +++ b/lib/sippy_cup/runner.rb @@ -15,6 +15,7 @@ class Runner # @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, true, false] :sudo Whether or not to invoke SIPp with sudo. 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. # @@ -22,7 +23,7 @@ def initialize(scenario, opts = {}) @scenario = scenario @scenario_options = @scenario.scenario_options - defaults = { full_sipp_output: true } + defaults = { full_sipp_output: true, sudo: true } @options = defaults.merge(opts) @command = @options[:command] @@ -87,7 +88,7 @@ def wait def command @command ||= begin - command = "sudo $(which sipp)" + command = @options[:sudo] ? "sudo $(which sipp)" : 'sipp' command_options.each_pair do |key, value| command << (value ? " -#{key} #{value}" : " -#{key}") end diff --git a/lib/sippy_cup/scenario.rb b/lib/sippy_cup/scenario.rb index cf31b25..2f2c7ae 100644 --- a/lib/sippy_cup/scenario.rb +++ b/lib/sippy_cup/scenario.rb @@ -2,6 +2,7 @@ require 'nokogiri' require 'psych' require 'active_support/core_ext/hash' +require 'active_support/core_ext/object/blank' require 'tempfile' require 'set' @@ -659,7 +660,7 @@ def to_xml(options = {}) @media_nodes.reverse.each do |nop| nopdup = docdup.xpath(nop.path) - if pcap_path.nil? or @media.empty? + if pcap_path.nil? or @media.blank? nopdup.remove else exec = nopdup.xpath("./action/exec").first @@ -694,7 +695,7 @@ def to_xml(options = {}) # scenario.compile! # Leaves files at test_scenario.xml and test_scenario.pcap # def compile! - unless @media.nil? + unless @media.blank? print "Compiling media to #{@filename}.pcap..." compile_media.to_file filename: "#{@filename}.pcap" puts "done." @@ -710,6 +711,80 @@ def compile! scenario_filename end + ## + # Shortcut method that tells SIPp to receive and accept a REFER + def wait_for_refer(opts = {}) + rrs = opts.delete :rrs + rrs = false if rrs.nil? + receive_refer(opts) + ack_refer(opts) + notify_refer_ringing(opts) + receive_200(opts.merge :rrs => rrs) + notify_refer_ok(opts) + receive_200(opts.merge :rrs => rrs) + end + + def receive_refer(opts = {}) + @scenario << new_recv(opts.merge request: 'REFER') + end + + def ack_refer(opts = {}) + msg = <<-ACK + + SIP/2.0 202 Accepted + Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch] + [last_From:] + [last_To:] + [last_Call-ID:] + [last_CSeq:] + Contact: + Max-Forwards: 100 + Content-Length: 0 + [routes] + ACK + @scenario << new_send(msg, opts) + end + + def notify_refer_ringing(opts = {}) + opts.merge! :body => 'SIP/2.0 180 Ringing', + :state => 'active' + notify_refer(opts) + end + + def notify_refer_ok(opts = {}) + opts.merge! :body => 'SIP/2.0 200 OK', + :state => 'terminated;reason=noresource' + notify_refer(opts) + end + + def notify_refer(opts = {}) + # @TODO The Event: field MUST contain an ID if multiple REFERs are issued + # @see http://tools.ietf.org/html/rfc3515#section-2.4.6 + body = opts.delete(:body) || (raise ArgumentError, + 'Each NOTIFY must include a body of type of type "message/sipfrag"') + state = opts.delete(:state) || (raise ArgumentError, + 'Each NOTIFY must contain a Subscription-State') + msg = <<-HEREDOC + + NOTIFY [next_url] SIP/2.0 + Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch] + From: "#{@from_user}" ;tag=[call_number] + To: [peer_tag_param] + [last_Call-ID:] + CSeq: [cseq] NOTIFY + Event: refer + Subscription-state: #{state} + Content-Type: message/sipfrag;version=2.0 + Contact: + Max-Forwards: 100 + Content-Length: [len] + [routes] + + #{body} + HEREDOC + @scenario << new_send(msg, opts) + end + # # Write compiled Scenario XML and PCAP media (if applicable) to tempfiles. # @@ -720,7 +795,7 @@ def compile! # @see http://www.ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/Tempfile.html # def to_tmpfiles - unless @media.nil? || @media.empty? + unless @media.blank? media_file = Tempfile.new 'media' media_file.binmode media_file.write compile_media.to_s diff --git a/sippy_cup.gemspec b/sippy_cup.gemspec index e62c56f..40ac33e 100644 --- a/sippy_cup.gemspec +++ b/sippy_cup.gemspec @@ -18,13 +18,13 @@ Gem::Specification.new do |s| s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = ["lib"] - s.add_runtime_dependency 'packetfu' - s.add_runtime_dependency 'nokogiri', ["~> 1.6.0"] + s.add_runtime_dependency 'packetfu', [">= 1.1.13"] + s.add_runtime_dependency 'nokogiri', ["~> 1.10.4"] s.add_runtime_dependency 'activesupport', [">= 3.0"] s.add_runtime_dependency 'psych', ["~> 2.0.1"] unless RUBY_PLATFORM == 'java' s.add_development_dependency 'guard-rspec' - s.add_development_dependency 'rspec', ["~> 2.11"] + s.add_development_dependency 'rspec', ["~> 3.4"] s.add_development_dependency 'simplecov' s.add_development_dependency 'simplecov-rcov' s.add_development_dependency 'fakefs' diff --git a/spec/sippy_cup/media_spec.rb b/spec/sippy_cup/media_spec.rb index 978a84c..c867065 100644 --- a/spec/sippy_cup/media_spec.rb +++ b/spec/sippy_cup/media_spec.rb @@ -11,7 +11,7 @@ end it 'should start with an empty sequence' do - @media.sequence.should be_empty + expect(@media.sequence).to be_empty end it 'should correctly report itself as empty' do @@ -25,7 +25,7 @@ it 'should append a valid action to the sequence list' do @media << 'silence:1000' - @media.sequence.include?('silence:1000').should be true + expect(@media.sequence.include?('silence:1000')).to be true end it 'should raise an error when assigning an invalid action' do @@ -35,30 +35,76 @@ it 'should produce a PcapFile containing 10 packets for 200ms of silence' do @media << 'silence:200' pf = @media.compile! - pf.body.count.should be 10 + expect(pf.body.count).to be 10 end it 'should produce a PcapPacket with 20ms of silence at the end' do @media << 'silence:20' pf = @media.compile! packet = pf.body.first - packet.class.should be PacketFu::PcapPacket - packet.data[-160, 160].should == 0xff.chr * 160 + expect(packet.class).to be PacketFu::PcapPacket + expect(packet.data[-160, 160]).to eq(0xff.chr * 160) end it 'should produce a PcapPacket with DTMF digit 3, volume 10 at the end' do @media << 'dtmf:3' pf = @media.compile! packet = pf.body.first - packet.class.should be PacketFu::PcapPacket - packet.data[-4, 4].should == ['030a00a0'].pack('H*') + expect(packet.class).to be PacketFu::PcapPacket + expect(packet.data[-4, 4]).to eq(['030a00a0'].pack('H*')) end it 'should produce a PcapPacket with DTMF digit #, volume 10 at the end' do @media << 'dtmf:#' pf = @media.compile! packet = pf.body.first - packet.class.should be PacketFu::PcapPacket - packet.data[-4, 4].should == ['0b0a00a0'].pack('H*') + expect(packet.class).to be PacketFu::PcapPacket + expect(packet.data[-4, 4]).to eq(['0b0a00a0'].pack('H*')) end + + it 'should generate 15 packets representing a DTMF digit' do + @media << 'dtmf:1' + pf = @media.compile! + + expect(pf.body.size).to eq 15 + end + + it 'should not set the end-of-event flag on the first 12 packets representing a DTMF digit' do + @media << 'dtmf:1' + pf = @media.compile! + + packets=pf.body.select{|packet| packet.data[-3,1] == ['0a'].pack('H*')} + expect(packets).to eq pf.body[0..11] + end + + it 'should set the end-of-event flag on the last 3 packets representing a DTMF digit' do + @media << 'dtmf:1' + pf = @media.compile! + + packets=pf.body.select{|packet| packet.data[-3,1] == ['8a'].pack('H*')} + expect(packets).to eq pf.body[12..14] + end + + it 'should generate a 250 ms long DTFM event' do + @media << 'dtmf:1' + pf = @media.compile! + + start_time = pf.body.first.timestamp.sec.to_f + (0.000001*pf.body.first.timestamp.usec.to_f) + end_time = pf.body.last.timestamp.sec.to_f + (0.000001*pf.body.last.timestamp.usec.to_f) + + expect(end_time - start_time).to be_within(0.01).of(0.250) + end + + it 'should generate RTP packets representing 20ms slices of a DTMF digit' do + @media << 'dtmf:1' + pf = @media.compile! + + expected_durations= + 12.times.map {|i| 160*(i+1)} + # body of event come in multiples of 160 rtp timestamp units (20ms) + 3.times.map { 160*13 } # 3 redundant end of event packets + + expect(pf.body.map{|packet| packet.data[-2,2].unpack('H*')[0].to_i(16)}).to eq expected_durations + end + + end diff --git a/spec/sippy_cup/runner_spec.rb b/spec/sippy_cup/runner_spec.rb index 41a9564..017eaf0 100644 --- a/spec/sippy_cup/runner_spec.rb +++ b/spec/sippy_cup/runner_spec.rb @@ -13,7 +13,7 @@ let(:logger) { double } - before { logger.stub :info } + before { allow(logger).to receive :info } let(:manifest) do <<-MANIFEST @@ -36,10 +36,10 @@ subject { SippyCup::Runner.new scenario, default_settings.merge(settings) } def expect_command_execution(command = anything) - Process.stub :wait2 - subject.stub :process_exit_status + allow(Process).to receive :wait2 + allow(subject).to receive :process_exit_status - subject.should_receive(:spawn).with(command, anything) + expect(subject).to receive(:spawn).with(command, anything) end describe '#run' do @@ -49,12 +49,21 @@ def expect_command_execution(command = anything) subject.run end + context 'with sudo disabled' do + let(:settings) { {sudo: false} } + it "executes the correct command to invoke SIPp" do + full_scenario_path = File.join(Dir.tmpdir, '/scenario.*') + expect_command_execution %r{sipp -p 8836 -sf #{full_scenario_path} -i dah.com bar.com} + subject.run + end + end + it "ensures that input files are not left on the filesystem" do FakeFS do Dir.mkdir("/tmp") unless Dir.exist?("/tmp") expect_command_execution.and_raise expect { subject.run }.to raise_error - Dir.entries(Dir.tmpdir).should eql(['.', '..']) + expect(Dir.entries(Dir.tmpdir)).to eql(['.', '..']) end end @@ -73,9 +82,9 @@ def expect_command_execution(command = anything) context "async" do let(:settings) { {async: true} } it 'should not wait for SIPp to terminate' do - subject.stub :process_exit_status - subject.should_receive :spawn - Process.should_not_receive :wait2 + allow(subject).to receive :process_exit_status + expect(subject).to receive :spawn + expect(Process).not_to receive :wait2 subject.run end end @@ -281,14 +290,14 @@ def expect_command_execution(command = anything) it 'logs the path to the csv file' do expect_command_execution - logger.should_receive(:info).with "Statistics logged at #{File.expand_path('stats.csv')}" + expect(logger).to receive(:info).with "Statistics logged at #{File.expand_path('stats.csv')}" subject.run end end context "no stats file" do it 'does not log a statistics file path' do - logger.should_receive(:info).with(/Statistics logged at/).never + expect(logger).to receive(:info).with(/Statistics logged at/).never expect_command_execution subject.run end @@ -404,8 +413,8 @@ def expect_command_execution(command = anything) end it 'uses CSV in the test run' do - logger.should_receive(:info).ordered.with(/Preparing to run SIPp command/) - logger.should_receive(:info).ordered.with(/Test completed successfully/) + expect(logger).to receive(:info).ordered.with(/Preparing to run SIPp command/) + expect(logger).to receive(:info).ordered.with(/Test completed successfully/) expect_command_execution(%r{-inf /path/to/vars.csv}) subject.run end @@ -450,7 +459,7 @@ def expect_command_execution(command = anything) let(:exit_code) { 0 } it "doesn't raise anything if SIPp returns 0" do - subject.run.should be true + expect(subject.run).to be true end end @@ -458,8 +467,8 @@ def expect_command_execution(command = anything) let(:exit_code) { 1 } it "returns false if SIPp returns 1" do - logger.should_receive(:info).ordered.with(/Test completed successfully but some calls failed./) - subject.run.should be false + expect(logger).to receive(:info).ordered.with(/Test completed successfully but some calls failed./) + expect(subject.run).to be false end end @@ -521,11 +530,11 @@ def active_thread_count context "by default" do it "proxies stdout to the terminal" do - capture(:stdout) { subject.run }.strip.should == output_string + expect(capture(:stdout) { subject.run }.strip).to eq(output_string) end it "proxies stderr to the terminal" do - capture(:stderr) { subject.run }.strip.should == error_string + expect(capture(:stderr) { subject.run }.strip).to include(error_string) end it "does not leak threads" do @@ -534,7 +543,7 @@ def active_thread_count original_thread_count = active_thread_count subject.run sleep 0.1 - active_thread_count.should == original_thread_count + expect(active_thread_count).to eq(original_thread_count) end end @@ -542,11 +551,11 @@ def active_thread_count let(:settings) { { command: command, full_sipp_output: false } } it "swallows stdout from SIPp" do - capture(:stdout) { subject.run }.should == '' + expect(capture(:stdout) { subject.run }).to eq('') end it "swallows stderr from SIPp" do - capture(:stderr) { subject.run }.should == '' + expect(capture(:stderr) { subject.run }).to eq('') end it "does not leak threads" do @@ -555,7 +564,7 @@ def active_thread_count original_thread_count = active_thread_count subject.run sleep 0.1 - active_thread_count.should == original_thread_count + expect(active_thread_count).to eq(original_thread_count) end end end @@ -564,18 +573,18 @@ def active_thread_count describe '#wait' do before { subject.sipp_pid = pid } it "waits for the SIPp process" do - Process.should_receive(:wait2).with pid.to_i - subject.should_receive(:process_exit_status) - subject.should_receive(:cleanup_input_files) + expect(Process).to receive(:wait2).with pid.to_i + expect(subject).to receive(:process_exit_status) + expect(subject).to receive(:cleanup_input_files) subject.wait end context "async" do subject { SippyCup::Runner.new scenario, logger: logger, async: true } it "waits for the SIPp process and cleans up input files" do - Process.should_receive(:wait2).with pid.to_i - subject.should_receive(:process_exit_status) - subject.should_receive(:cleanup_input_files) + expect(Process).to receive(:wait2).with pid.to_i + expect(subject).to receive(:process_exit_status) + expect(subject).to receive(:cleanup_input_files) subject.wait end end @@ -585,7 +594,7 @@ def active_thread_count before { subject.sipp_pid = pid } it "tries to kill the SIPp process if there is a PID" do - Process.should_receive(:kill).with("KILL", pid) + expect(Process).to receive(:kill).with("KILL", pid) subject.stop end @@ -593,18 +602,18 @@ def active_thread_count let(:pid) { nil } it "doesn't try to kill the SIPp process" do - Process.should_receive(:kill).never + expect(Process).to receive(:kill).never subject.stop end end it "raises a Errno::ESRCH if the PID does not exist" do - Process.should_receive(:kill).with("KILL", pid).and_raise(Errno::ESRCH) + expect(Process).to receive(:kill).with("KILL", pid).and_raise(Errno::ESRCH) expect { subject.stop }.to raise_error Errno::ESRCH end it "raises a Errno::EPERM if the user has no permission to kill the process" do - Process.should_receive(:kill).with("KILL", pid).and_raise(Errno::EPERM) + expect(Process).to receive(:kill).with("KILL", pid).and_raise(Errno::EPERM) expect { subject.stop }.to raise_error Errno::EPERM end end diff --git a/spec/sippy_cup/scenario_spec.rb b/spec/sippy_cup/scenario_spec.rb index d042643..1f7a2ea 100644 --- a/spec/sippy_cup/scenario_spec.rb +++ b/spec/sippy_cup/scenario_spec.rb @@ -19,59 +19,59 @@ invite end - s.to_xml.should =~ %r{INVITE sip:\[service\]@\[remote_ip\]:\[remote_port\] SIP/2.0} + expect(s.to_xml).to match(%r{INVITE sip:\[service\]@\[remote_ip\]:\[remote_port\] SIP/2.0}) end it "allows creating a blank scenario with no block" do - subject.to_xml.should =~ %r{} + expect(subject.to_xml).to match(%r{}) end describe '#invite' do it "sends an INVITE message" do subject.invite - subject.to_xml.should match(%r{}) - subject.to_xml.should match(%r{INVITE}) + expect(subject.to_xml).to match(%r{}) + expect(subject.to_xml).to match(%r{INVITE}) end it "allows setting options on the send instruction" do subject.invite foo: 'bar' - subject.to_xml.should match(%r{}) + expect(subject.to_xml).to match(%r{}) end it "defaults to retrans of 500" do subject.invite - subject.to_xml.should match(%r{}) + expect(subject.to_xml).to match(%r{}) end it "allows setting retrans" do subject.invite retrans: 200 - subject.to_xml.should match(%r{}) + expect(subject.to_xml).to match(%r{}) end context "with extra headers specified" do it "adds the headers to the end of the message" do subject.invite headers: "Foo: \nBar: " - subject.to_xml.should match(%r{Foo: \nBar: }) + expect(subject.to_xml).to match(%r{Foo: \nBar: }) end it "only has one blank line between headers and SDP" do subject.invite headers: "Foo: \n\n\n" - subject.to_xml.should match(%r{Foo: \n\nv=0}) + expect(subject.to_xml).to match(%r{Foo: \n\nv=0}) end end context "with no extra headers" do it "only has one blank line between headers and SDP" do subject.invite - subject.to_xml.should match(%r{Content-Length: \[len\]\n\nv=0}) + expect(subject.to_xml).to match(%r{Content-Length: \[len\]\n\nv=0}) end end it "uses [media_port+1] as the RTCP port in the SDP" do subject.invite - subject.to_xml.should match(%r{m=audio \[media_port\] RTP/AVP 0 101\n}) + expect(subject.to_xml).to match(%r{m=audio \[media_port\] RTP/AVP 0 101\n}) end context "when a from user is specified" do @@ -79,16 +79,16 @@ it "includes the specified user in the From and Contact headers" do subject.invite - subject.to_xml.should match(%r{From: "frank" }) - subject.to_xml.should match(%r{REGISTER}) + expect(subject.to_xml).to match(%r{}) + expect(subject.to_xml).to match(%r{REGISTER}) end it "allows setting options on the send instruction" do subject.register 'frank', nil, foo: 'bar' - subject.to_xml.should match(%r{}) + expect(subject.to_xml).to match(%r{}) end it "defaults to retrans of 500" do subject.register 'frank' - subject.to_xml.should match(%r{}) + expect(subject.to_xml).to match(%r{}) end it "allows setting retrans" do subject.register 'frank', nil, retrans: 200 - subject.to_xml.should match(%r{}) + expect(subject.to_xml).to match(%r{}) end context "when a domain is provided" do it "uses the specified user and domain" do subject.register 'frank@foobar.com' - subject.to_xml.should match(%r{REGISTER sip:foobar.com}) - subject.to_xml.should match(%r{From: }) + expect(subject.to_xml).to match(%r{}) + fail "Not yet implemented" end it "adds authentication data to the REGISTER message" do subject.register 'frank', 'abc123' - subject.to_xml.should match(%r{\[authentication username=frank password=abc123\]}) + expect(subject.to_xml).to match(%r{\[authentication username=frank password=abc123\]}) end end end @@ -182,19 +183,19 @@ it "expects an optional 100" do subject.receive_trying - scenario.to_xml.should match(%q{}) + expect(scenario.to_xml).to match(%q{}) end it "allows passing options to the recv expectation" do subject.receive_trying foo: 'bar' - scenario.to_xml.should match(%q{}) + expect(scenario.to_xml).to match(%q{}) end it "allows overriding options" do subject.receive_trying optional: false - scenario.to_xml.should match(%q{}) + expect(scenario.to_xml).to match(%q{}) end end @@ -202,19 +203,19 @@ it "expects an optional 180" do subject.receive_ringing - scenario.to_xml.should match(%q{}) + expect(scenario.to_xml).to match(%q{}) end it "allows passing options to the recv expectation" do subject.receive_ringing foo: 'bar' - scenario.to_xml.should match(%q{}) + expect(scenario.to_xml).to match(%q{}) end it "allows overriding options" do subject.receive_ringing optional: false - scenario.to_xml.should match(%q{}) + expect(scenario.to_xml).to match(%q{}) end end @@ -222,19 +223,19 @@ it "expects an optional 183" do subject.receive_progress - scenario.to_xml.should match(%q{}) + expect(scenario.to_xml).to match(%q{}) end it "allows passing options to the recv expectation" do subject.receive_progress foo: 'bar' - scenario.to_xml.should match(%q{}) + expect(scenario.to_xml).to match(%q{}) end it "allows overriding options" do subject.receive_progress optional: false - scenario.to_xml.should match(%q{}) + expect(scenario.to_xml).to match(%q{}) end end @@ -242,19 +243,19 @@ it "expects a 200 with rrs and rtd true" do subject.receive_answer - scenario.to_xml.should match(%q{}) + expect(scenario.to_xml).to match(%q{}) end it "allows passing options to the recv expectation" do subject.receive_answer foo: 'bar' - scenario.to_xml.should match(%q{}) + expect(scenario.to_xml).to match(%q{}) end it "allows overriding options" do subject.receive_answer rtd: false - scenario.to_xml.should match(%q{}) + expect(scenario.to_xml).to match(%q{}) end end @@ -262,19 +263,19 @@ it "expects a 200" do subject.receive_200 - scenario.to_xml.should match(%q{}) + expect(scenario.to_xml).to match(%q{}) end it "allows passing options to the recv expectation" do subject.receive_200 foo: 'bar' - scenario.to_xml.should match(%q{}) + expect(scenario.to_xml).to match(%q{}) end it "allows overriding options" do subject.receive_200 response: 999 # Silly but still... - scenario.to_xml.should match(%q{}) + expect(scenario.to_xml).to match(%q{}) end end @@ -282,13 +283,13 @@ it "sends an ACK message" do subject.ack_answer - subject.to_xml.should match(%r{}) - subject.to_xml.should match(%r{ACK}) + expect(subject.to_xml).to match(%r{}) + expect(subject.to_xml).to match(%r{ACK}) end it "allows setting options on the send instruction" do subject.ack_answer foo: 'bar' - subject.to_xml.should match(%r{}) + expect(subject.to_xml).to match(%r{}) end context "when media is present" do @@ -300,14 +301,14 @@ it "starts the PCAP media" do subject.ack_answer subject.sleep 1 - subject.to_xml(:pcap_path => "/dev/null").should match(%r{\n.*\n.*\n.*\n.*}) + expect(subject.to_xml(:pcap_path => "/dev/null")).to match(%r{\n.*\n.*\n.*\n.*}) end end context "when media is not present" do it "does not start the PCAP media" do subject.ack_answer - subject.to_xml(:pcap_path => "/dev/null").should_not match(%r{\n.*\n.*\n.*\n.*}) + expect(subject.to_xml(:pcap_path => "/dev/null")).not_to match(%r{\n.*\n.*\n.*\n.*}) end end end @@ -317,49 +318,49 @@ scenario.wait_for_answer xml = scenario.to_xml - xml.should =~ /recv response="100".*optional="true"/ - xml.should =~ /recv response="180".*optional="true"/ - xml.should =~ /recv response="183".*optional="true"/ - xml.should =~ /recv response="200"/ - xml.should_not =~ /recv response="200".*optional="true"/ - xml.should match(%r{}) - xml.should match(%r{ACK}) + expect(xml).to match(/recv response="100".*optional="true"/) + expect(xml).to match(/recv response="180".*optional="true"/) + expect(xml).to match(/recv response="183".*optional="true"/) + expect(xml).to match(/recv response="200"/) + expect(xml).not_to match(/recv response="200".*optional="true"/) + expect(xml).to match(%r{}) + expect(xml).to match(%r{ACK}) end it "passes through additional options" do scenario.wait_for_answer foo: 'bar' xml = scenario.to_xml - xml.should =~ /recv .*foo="bar".*response="100"/ - xml.should =~ /recv .*foo="bar".*response="180"/ - xml.should =~ /recv .*foo="bar".*response="183"/ - xml.should =~ /recv .*response="200" .*foo="bar"/ - xml.should match(%r{}) - xml.should match(%r{ACK}) + expect(xml).to match(/recv .*foo="bar".*response="100"/) + expect(xml).to match(/recv .*foo="bar".*response="180"/) + expect(xml).to match(/recv .*foo="bar".*response="183"/) + expect(xml).to match(/recv .*response="200" .*foo="bar"/) + expect(xml).to match(%r{}) + expect(xml).to match(%r{ACK}) end end describe '#receive_message' do it "expects a MESSAGE and acks it" do subject.receive_message - subject.to_xml.should match(%r{.*SIP/2\.0 200 OK}m) + expect(subject.to_xml).to match(%r{.*SIP/2\.0 200 OK}m) end it "allows a string to be given as a regexp for matching" do subject.receive_message "Hello World!" - subject.to_xml.should match(%r{\s*\s*}m) + expect(subject.to_xml).to match(%r{\s*\s*}m) end it "increments the variable name used for regexp matching because SIPp requires it to be unique" do subject.receive_message "Hello World!" subject.receive_message "Hello Again World!" subject.receive_message "Goodbye World!" - subject.to_xml.should match(%r{]* assign_to="([^"]+)_1"/>.*]* assign_to="\1_2"/>.*]* assign_to="\1_3"/>}m) + expect(subject.to_xml).to match(%r{]* assign_to="([^"]+)_1"/>.*]* assign_to="\1_2"/>.*]* assign_to="\1_3"/>}m) end it "declares the variable used for regexp matching so that SIPp doesn't complain that it's unused" do subject.receive_message "Hello World!" - subject.to_xml.should match(%r{]* assign_to="([^"]+)"/>.*}m) + expect(subject.to_xml).to match(%r{]* assign_to="([^"]+)"/>.*}m) end end @@ -367,13 +368,13 @@ it "sends a BYE message" do subject.send_bye - subject.to_xml.should match(%r{}) - subject.to_xml.should match(%r{BYE}) + expect(subject.to_xml).to match(%r{}) + expect(subject.to_xml).to match(%r{BYE}) end it "allows setting options on the send instruction" do subject.send_bye foo: 'bar' - subject.to_xml.should match(%r{}) + expect(subject.to_xml).to match(%r{}) end end @@ -381,13 +382,13 @@ it "expects a BYE" do subject.receive_bye - scenario.to_xml.should match(%q{}) + expect(scenario.to_xml).to match(%q{}) end it "allows passing options to the recv expectation" do subject.receive_bye foo: 'bar' - scenario.to_xml.should match(%q{}) + expect(scenario.to_xml).to match(%q{}) end end @@ -395,13 +396,13 @@ it "sends a 200 OK" do subject.okay - subject.to_xml.should match(%r{}) - subject.to_xml.should match(%r{SIP/2.0 200 OK}) + expect(subject.to_xml).to match(%r{}) + expect(subject.to_xml).to match(%r{SIP/2.0 200 OK}) end it "allows setting options on the send instruction" do subject.okay foo: 'bar' - subject.to_xml.should match(%r{}) + expect(subject.to_xml).to match(%r{}) end end @@ -409,71 +410,71 @@ it "expects a BYE and acks it" do subject.receive_bye foo: 'bar' - scenario.to_xml.should match(%q{}) - scenario.to_xml.should match(%q{}) + expect(scenario.to_xml).to match(%q{}) + expect(scenario.to_xml).to match(%q{}) end end describe '#call_length_repartition' do it 'create a partition table' do subject.call_length_repartition('1', '10', '2') - scenario.to_xml.should match('') + expect(scenario.to_xml).to match('') end end describe '#response_time_repartition' do it 'create a partition table' do subject.response_time_repartition('1', '10', '2') - scenario.to_xml.should match('') + expect(scenario.to_xml).to match('') end end describe 'media-dependent operations' do let(:media) { double :media } before do - SippyCup::Media.should_receive(:new).once.and_return media + expect(SippyCup::Media).to receive(:new).once.and_return media scenario.ack_answer - media.stub :<< + allow(media).to receive :<< end describe '#sleep' do it "creates the proper amount of silent audio'" do - media.should_receive(:<<).once.with 'silence:5000' + expect(media).to receive(:<<).once.with 'silence:5000' scenario.sleep 5 end it "should insert a pause into the scenario" do scenario.sleep 5 - scenario.to_xml.should match(%r{}) + expect(scenario.to_xml).to match(%r{}) end context "when passed fractional seconds" do it "creates the proper amount of silent audio" do - media.should_receive(:<<).once.with 'silence:500' + expect(media).to receive(:<<).once.with 'silence:500' scenario.sleep '0.5' end it "should insert a pause into the scenario" do scenario.sleep 0.5 - scenario.to_xml.should match(%r{}) + expect(scenario.to_xml).to match(%r{}) end end end describe '#send_digits' do it "creates the requested DTMF string in media, with 250ms pauses between" do - media.should_receive(:<<).ordered.with 'dtmf:1' - media.should_receive(:<<).ordered.with 'silence:250' - media.should_receive(:<<).ordered.with 'dtmf:3' - media.should_receive(:<<).ordered.with 'silence:250' - media.should_receive(:<<).ordered.with 'dtmf:6' - media.should_receive(:<<).ordered.with 'silence:250' + expect(media).to receive(:<<).ordered.with 'dtmf:1' + expect(media).to receive(:<<).ordered.with 'silence:250' + expect(media).to receive(:<<).ordered.with 'dtmf:3' + expect(media).to receive(:<<).ordered.with 'silence:250' + expect(media).to receive(:<<).ordered.with 'dtmf:6' + expect(media).to receive(:<<).ordered.with 'silence:250' scenario.send_digits '136' end it "should insert a pause into the scenario to cover the DTMF duration (250ms) and the pause" do scenario.send_digits '136' - scenario.to_xml.should match(%r{}) + expect(scenario.to_xml).to match(%r{}) end end end @@ -486,18 +487,18 @@ scenario.send_digits '136' xml = scenario.to_xml - scenario.to_xml.should match(%r{(.*INFO \[next_url\] SIP/2\.0.*.*){3}}m) - scenario.to_xml.should match(%r{Signal=1(\nDuration=250\n).*Signal=3\1.*Signal=6\1}m) + expect(scenario.to_xml).to match(%r{(.*INFO \[next_url\] SIP/2\.0.*.*){3}}m) + expect(scenario.to_xml).to match(%r{Signal=1(\nDuration=250\n).*Signal=3\1.*Signal=6\1}m) end it "expects a response for each digit sent" do scenario.send_digits '123' - scenario.to_xml.should match(%r{(.*INFO.*.*.*){3}}m) + expect(scenario.to_xml).to match(%r{(.*INFO.*.*.*){3}}m) end it "inserts 250ms pauses between each digit" do scenario.send_digits '321' - scenario.to_xml.should match(%r{(.*INFO.*.*.*){3}}m) + expect(scenario.to_xml).to match(%r{(.*INFO.*.*.*){3}}m) end end @@ -508,7 +509,7 @@ scenario.compile! - File.read("/tmp/test.xml").should == scenario.to_xml + expect(File.read("/tmp/test.xml")).to eq(scenario.to_xml) end it "writes the PCAP media to disk at name.pcap" do @@ -517,11 +518,11 @@ scenario.compile! - File.read("/tmp/test.pcap").should_not be_empty + expect(File.read("/tmp/test.pcap")).not_to be_empty end it "returns the path to the scenario file" do - scenario.compile!.should == "/tmp/test.xml" + expect(scenario.compile!).to eq("/tmp/test.xml") end end @@ -533,7 +534,7 @@ scenario.compile! - File.read("/tmp/foobar.xml").should == scenario.to_xml + expect(File.read("/tmp/foobar.xml")).to eq(scenario.to_xml) end it "writes the PCAP media to disk at filename.pcap" do @@ -542,11 +543,11 @@ scenario.compile! - File.read("/tmp/foobar.pcap").should_not be_empty + expect(File.read("/tmp/foobar.pcap")).not_to be_empty end it "returns the path to the scenario file" do - scenario.compile!.should == "/tmp/foobar.xml" + expect(scenario.compile!).to eq("/tmp/foobar.xml") end end end @@ -556,19 +557,19 @@ it "writes the scenario XML to a Tempfile and returns it" do files = scenario.to_tmpfiles - files[:scenario].should be_a(Tempfile) - files[:scenario].read.should eql(scenario.to_xml) + expect(files[:scenario]).to be_a(Tempfile) + expect(files[:scenario].read).to eql(scenario.to_xml) end it "allows the scenario XML to be read from disk independently" do files = scenario.to_tmpfiles - File.read(files[:scenario].path).should eql(scenario.to_xml) + expect(File.read(files[:scenario].path)).to eql(scenario.to_xml) end context "without media" do it "does not write a PCAP media file" do files = scenario.to_tmpfiles - files[:media].should be_nil + expect(files[:media]).to be_nil end end @@ -580,18 +581,18 @@ it "writes the PCAP media to a Tempfile and returns it" do files = scenario.to_tmpfiles - files[:media].should be_a(Tempfile) - files[:media].read.should_not be_empty + expect(files[:media]).to be_a(Tempfile) + expect(files[:media].read).not_to be_empty end it "allows the PCAP media to be read from disk independently" do files = scenario.to_tmpfiles - File.read(files[:media].path).should_not be_empty + expect(File.read(files[:media].path)).not_to be_empty end it "puts the PCAP file path into the scenario XML" do files = scenario.to_tmpfiles - files[:scenario].read.should match(%r{play_pcap_audio="#{files[:media].path}"}) + expect(files[:scenario].read).to match(%r{play_pcap_audio="#{files[:media].path}"}) end end end @@ -674,7 +675,7 @@ it "runs each step" do subject.build(steps) - subject.to_xml(:pcap_path => "/dev/null").should == scenario_xml + expect(subject.to_xml(:pcap_path => "/dev/null")).to eq(scenario_xml) end end @@ -688,9 +689,9 @@ end it "each method should receive the correct arguments" do - subject.should_receive(:register).once.ordered.with('user@domain.com', 'my password has spaces') - subject.should_receive(:sleep).once.ordered.with('3') - subject.should_receive(:send_digits).once.ordered.with('12345') + expect(subject).to receive(:register).once.ordered.with('user@domain.com', 'my password has spaces') + expect(subject).to receive(:sleep).once.ordered.with('3') + expect(subject).to receive(:send_digits).once.ordered.with('12345') subject.build steps end end @@ -811,12 +812,12 @@ it "generates the correct XML" do scenario = described_class.from_manifest(scenario_yaml) - scenario.to_xml(:pcap_path => "/dev/null").should == scenario_xml + expect(scenario.to_xml(:pcap_path => "/dev/null")).to eq(scenario_xml) end it "sets the proper options" do scenario = described_class.from_manifest(scenario_yaml) - scenario.scenario_options.should == { + expect(scenario.scenario_options).to eq({ 'name' => 'spec scenario', 'source' => '192.0.2.15', 'destination' => '192.0.2.200', @@ -824,7 +825,7 @@ 'calls_per_second' => 5, 'number_of_calls' => 20, 'from_user' => "#{specs_from}" - } + }) end context "when the :scenario key is provided in the manifest" do @@ -845,8 +846,8 @@ it "creates an XMLScenario with the scenario XML and nil media" do scenario = described_class.from_manifest(scenario_yaml) - scenario.should be_a(SippyCup::XMLScenario) - scenario.to_xml.should == File.read(scenario_path) + expect(scenario).to be_a(SippyCup::XMLScenario) + expect(scenario.to_xml).to eq(File.read(scenario_path)) end context "and the :media key is provided" do @@ -870,7 +871,7 @@ media = File.read(media_path, mode: 'rb') files = scenario.to_tmpfiles - files[:media].read.should eql(media) + expect(files[:media].read).to eql(media) end end end @@ -896,7 +897,7 @@ it "should default to 'My Scenario'" do scenario = described_class.from_manifest(scenario_yaml) - scenario.scenario_options[:name].should == 'My Scenario' + expect(scenario.scenario_options[:name]).to eq('My Scenario') end end @@ -904,7 +905,7 @@ context "and a name in the manifest" do it "uses the name from the manifest" do scenario = described_class.from_manifest(scenario_yaml, input_filename: '/tmp/foobar.yml') - scenario.scenario_options[:name].should == 'spec scenario' + expect(scenario.scenario_options[:name]).to eq('spec scenario') end end @@ -929,7 +930,7 @@ it "uses the input filename" do scenario = described_class.from_manifest(scenario_yaml, input_filename: '/tmp/foobar.yml') - scenario.scenario_options[:name].should == 'foobar' + expect(scenario.scenario_options[:name]).to eq('foobar') end end end @@ -939,12 +940,12 @@ it "overrides keys with values from the options hash" do scenario = described_class.from_manifest(scenario_yaml, override_options) - scenario.to_xml(:pcap_path => "/dev/null").should == scenario_xml + expect(scenario.to_xml(:pcap_path => "/dev/null")).to eq(scenario_xml) end it "sets the proper options" do scenario = described_class.from_manifest(scenario_yaml, override_options) - scenario.scenario_options.should == { + expect(scenario.scenario_options).to eq({ 'name' => 'spec scenario', 'source' => '192.0.2.15', 'destination' => '192.0.2.200', @@ -952,7 +953,7 @@ 'calls_per_second' => 5, 'number_of_calls' => override_options[:number_of_calls], 'from_user' => "#{specs_from}" - } + }) end end @@ -982,12 +983,12 @@ it "sets the validity of the scenario" do scenario = SippyCup::Scenario.from_manifest(scenario_yaml) - scenario.should_not be_valid + expect(scenario).not_to be_valid end it "sets the error messages for the scenario" do scenario = SippyCup::Scenario.from_manifest(scenario_yaml) - scenario.errors.should == [{step: 4, message: "send_digits 'xyz': Invalid DTMF digit requested: x"}] + expect(scenario.errors).to eq([{step: 4, message: "send_digits 'xyz': Invalid DTMF digit requested: x"}]) end end end diff --git a/spec/sippy_cup/xml_scenario_spec.rb b/spec/sippy_cup/xml_scenario_spec.rb index 6d24c85..45bfebe 100644 --- a/spec/sippy_cup/xml_scenario_spec.rb +++ b/spec/sippy_cup/xml_scenario_spec.rb @@ -55,31 +55,31 @@ describe "#to_xml" do it "should return the XML representation of the scenario" do - subject.to_xml.should == xml + expect(subject.to_xml).to eq(xml) end end describe "#to_tmpfiles" do it "writes the scenario XML to a Tempfile and returns it" do files = scenario.to_tmpfiles - files[:scenario].should be_a(Tempfile) - files[:scenario].read.should eql(xml) + expect(files[:scenario]).to be_a(Tempfile) + expect(files[:scenario].read).to eql(xml) end it "allows the scenario XML to be read from disk independently" do files = scenario.to_tmpfiles - File.read(files[:scenario].path).should eql(xml) + expect(File.read(files[:scenario].path)).to eql(xml) end it "writes the PCAP media to a Tempfile and returns it" do files = scenario.to_tmpfiles - files[:media].should be_a(Tempfile) - files[:media].read.should eql(media) + expect(files[:media]).to be_a(Tempfile) + expect(files[:media].read).to eql(media) end it "allows the PCAP media to be read from disk independently" do files = scenario.to_tmpfiles - File.read(files[:media].path).should eql(media) + expect(File.read(files[:media].path)).to eql(media) end context "when media is not provided" do @@ -87,18 +87,18 @@ it "should not create a media file" do files = scenario.to_tmpfiles - files[:media].should be_nil + expect(files[:media]).to be_nil end end end describe "#scenario_options" do it "should return options passed to the initializer" do - scenario.scenario_options.should == { + expect(scenario.scenario_options).to eq({ name: 'Test', source: '127.0.0.1:5060', destination: '10.0.0.1:5080' - } + }) end end end