sippy_cup 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- ZTYzYWNiZmNmOTU4NjU5MzBhOTRhZGI4M2ZlZjg5YzBiOWEyZmIzNw==
5
- data.tar.gz: !binary |-
6
- NTU0NDIwOWUyZjc5MTRjMzkzOTQwZTNhZWZjNjhlNTMwZmM0N2NhYQ==
2
+ SHA1:
3
+ metadata.gz: 12ebcabfa7aeab3fd07ebd8ccf1a0f4cfb88b859
4
+ data.tar.gz: 917066d6774889765f8c19d40d694d85dc02c19a
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- ZWE1NDM0Nzg5OTU0NDAzMDA0MmI3Mjk0YjY1MWFkOTMzZDQ4YmU0ZTI0ZDk0
10
- N2ViNjlhYWVlOGQ4YTQxOGUxYTQ5NzE0ZjY4Y2ZlNjVlMjBhMTczZmMxZmFh
11
- YzAxMzU4MTk4YzYyZWZmN2EyZDRlYjMyYmZlNGYxMGY1NzdlYzg=
12
- data.tar.gz: !binary |-
13
- MmRiYTBlMjc4MzUxMjAwZjdmZGJjYWY1MDNjNGM2MTU5ZTEyNjk2MWE2ZTgy
14
- NWE1ZDM2YTQzNmYyZDg0NDUwNTk0NDEzZWZhYTg0OWIxNGY4NzIwMTQxNWM0
15
- MDk0MTEyZDhhYzIzNjU1YjIwMDMyYjU4ODJlNTkxMDI5YjZmZDk=
6
+ metadata.gz: 3cd3fab1459b55ccadc61c1d74a58c304aaf91c9ff27f406bd189b30dbcef88216caf78c73fce48a5317db64e6e1ed9cbeec4525dd15a0a3de86105ec914a87e
7
+ data.tar.gz: d712da0444fbc5774db1f983cb0277f14950971742f6fe5a09c61c9ee1ec5c1a9017de923b0951e3cd63a0b9ecbfd7f152bb9386171acfd1b7a95e1fdaa7a47a
data/.travis.yml CHANGED
@@ -2,12 +2,14 @@ language: ruby
2
2
  rvm:
3
3
  - 1.9.3
4
4
  - 2.0.0
5
+ - 2.1.0
5
6
  - jruby-19mode
6
7
  - rbx-19mode
7
8
  - ruby-head
8
9
  matrix:
9
10
  allow_failures:
10
11
  - rvm: 2.0.0
12
+ - rvm: 2.1.0
11
13
  - rvm: jruby-19mode
12
14
  - rvm: rbx-19mode
13
15
  - rvm: ruby-head
data/CHANGELOG.md CHANGED
@@ -1,4 +1,13 @@
1
- # develop
1
+ # [0.4.0](https://github.com/bklang/sippy_cup/compare/v0.3.0...v0.4.0)
2
+ * Feature: receive_message for incoming SIP MESSAGEs.
3
+ * Feature: SIP INFO DTMF.
4
+ * Feature: Don't write unnecessary PCAP files.
5
+ * Feature: Execute sipp via which, allowing sudo rule to be more restrictive.
6
+ * Change: Split `#receive_200` into its own method ([#61](https://github.com/mojolingo/sippy_cup/pull/61))
7
+ * Allow passing arbitrary SIPp options from the YAML manifest
8
+ * Bugfix: Fix ACK/BYE being sent to self.
9
+ * Bugfix: Require Psych 2.0.1 to fix `safe_load` NoMethodError (#63)
10
+ * Bugfix: Ensure the correct XML serializer is used (#66)
2
11
 
3
12
  # [0.3.0](https://github.com/bklang/sippy_cup/compare/v0.2.3...v0.3.0)
4
13
  * Feature: A whole lot more documentation, test coverage and cleaner internals.
@@ -8,7 +17,6 @@
8
17
  * Feature: API for validation of scenarios in manifests.
9
18
  * Feature: Handle SIPp exit codes with clean exceptions.
10
19
  * Feature: Allow passing arbitary headers in an INVITE
11
- * Change: AVP is now AVPF in SDP.
12
20
  * Change: Rake tasks for executing scenarios are removed.
13
21
  * Change: Running and compiling scenarios are now separate concepts.
14
22
  * `-c` on the CLI writes a YAML manifest to disk as SIPp XML and PCAP media. `-r` executes a YAML manifest and does not write to disk.
data/README.markdown CHANGED
@@ -33,13 +33,13 @@ Sippy Cup is a tool to generate [SIPp](http://sipp.sourceforge.net/) load test p
33
33
 
34
34
  SippyCup relies on the following to generate scenarios and the associated media PCAP files:
35
35
 
36
- * Ruby 1.9.3 (2.0.0 NOT YET SUPPORTED; see [PacketFu Issue #28](https://github.com/todb/packetfu/issues/28))
36
+ * Ruby 1.9.3 or later (2.1.2 recommended)
37
37
  * [SIPp](http://sipp.sourceforge.net/) - Download from http://sourceforge.net/projects/sipp/files/
38
38
  * "root" user access via sudo: needed to run SIPp so it can bind to raw network sockets
39
39
 
40
40
  ## Installation
41
41
 
42
- If you do not have Ruby 1.9.3 available (check using `ruby --version`), we recommend installing Ruby with [RVM](http://rvm.io)
42
+ If you do not have Ruby 2.1.2 available (check using `ruby --version`), we recommend installing Ruby with [RVM](http://rvm.io)
43
43
 
44
44
  ### Install via gem (production)
45
45
 
@@ -147,6 +147,8 @@ Each command below can take [SIPp attributes](http://sipp.sourceforge.net/doc/re
147
147
  * `wait_for_answer` Convenient shortcut for `receive_trying; receive_ringing; receive_progress; receive_answer`, with all but the `answer` marked as optional
148
148
  * `ack_answer` Send an `ACK` in response to a `200 OK`
149
149
  * `send_digits <string>` Send a DTMF string. May send one or many digits, including `0-9`, `*`, `#`, and `A-D`
150
+ * `receive_ok` Expect to receive a `200 OK`
151
+ * `receive_message [regex]` Expect to receive a SIP MESSAGE, optionally matching a regex
150
152
  * `send_bye` Send a `BYE` (hangup request)
151
153
  * `receive_bye` Expect to receive a `BYE` from the target
152
154
  * `ack_bye` Send a `200 OK` response to a `BYE`
@@ -184,15 +186,27 @@ Each parameter has an impact on the test, and may either be changed once the XML
184
186
  <dt>stats_interval</dt>
185
187
  <dd>Frequency (in seconds) of statistics collections. Defaults to 10. Has no effect unless :stats_file is also specified</dd>
186
188
 
187
- <dt>sip_user</dt>
188
- <dd>SIP username to use. Defaults to "1" (as in 1@127.0.0.1)</dd>
189
+ <dt>from_user</dt>
190
+ <dd>SIP user from which traffic should appear. Defaults to "sipp".</dd>
191
+
192
+ <dt>to_user</dt>
193
+ <dd>SIP user to send requests to. Defaults to "1" (as in 1@127.0.0.1).</dd>
194
+
195
+ <dt>transport</dt>
196
+ <dd>Specify the SIP transport. Valid options are `udp` (default) or `tcp`.</dd>
189
197
 
190
198
  <dt>full_sipp_output</dt>
191
199
  <dd>By default, SippyCup will show SIPp's command line output while running a scenario. Set this parameter to `false` to hide full command line output</dd>
192
200
 
201
+ <dt>options</dt>
202
+ <dd>A string of SIPp command line options included with the SIPp run.</dd>
203
+
193
204
  <dt>media_port</dt>
194
205
  <dd>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.</dd>
195
206
 
207
+ <dt>dtmf_mode</dt>
208
+ <dd>Specify the mechanism by which DTMF is signaled. Valid options are `rfc2833` for within the RTP media, or `info` for SIP INFO.</dd>
209
+
196
210
  <dt>scenario_variables</dt>
197
211
  <dd>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).)</dd>
198
212
  </dl>
@@ -217,7 +231,7 @@ For more information on possible attributes, visit the [SIPp Documentation](http
217
231
 
218
232
  ## Credits
219
233
 
220
- Copyright (C) 2013 [Mojo Lingo LLC](https://mojolingo.com)
234
+ Copyright (C) 2013-2014 [Mojo Lingo LLC](https://mojolingo.com)
221
235
 
222
236
  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.
223
237
 
@@ -8,7 +8,6 @@ module SippyCup
8
8
  USEC = 1_000_000
9
9
  MSEC = 1_000
10
10
  attr_accessor :sequence
11
- attr_reader :packets
12
11
 
13
12
  def initialize(from_addr, from_port, to_addr, to_port, generator = PCMUPayload)
14
13
  @from_addr, @to_addr = IPAddr.new(from_addr), IPAddr.new(to_addr)
@@ -18,7 +17,6 @@ module SippyCup
18
17
 
19
18
  def reset!
20
19
  @sequence = []
21
- @packets = []
22
20
  end
23
21
 
24
22
  def <<(input)
@@ -26,6 +24,10 @@ module SippyCup
26
24
  @sequence << input
27
25
  end
28
26
 
27
+ def empty?
28
+ @sequence.empty?
29
+ end
30
+
29
31
  def compile!
30
32
  sequence_number = 0
31
33
  start_time = Time.now
@@ -27,8 +27,34 @@ module SippyCup
27
27
  @logger = @options[:logger] || Logger.new(STDOUT)
28
28
  end
29
29
 
30
+ #
30
31
  # Runs the loaded scenario using SIPp
31
32
  #
33
+ def run
34
+ @input_files = @scenario.to_tmpfiles
35
+
36
+ @logger.info "Preparing to run SIPp command: #{command}"
37
+
38
+ execute_with_redirected_streams
39
+
40
+ wait unless @options[:async]
41
+ ensure
42
+ cleanup_input_files unless @options[:async]
43
+ end
44
+
45
+ #
46
+ # Tries to stop SIPp by killing the target PID
47
+ #
48
+ # @raises Errno::ESRCH when the PID does not correspond to a known process
49
+ # @raises Errno::EPERM when the process referenced by the PID cannot be killed
50
+ #
51
+ def stop
52
+ Process.kill "KILL", @sipp_pid if @sipp_pid
53
+ end
54
+
55
+ #
56
+ # Waits for the runner to finish execution
57
+ #
32
58
  # @raises Errno::ENOENT when the SIPp executable cannot be found
33
59
  # @raises SippyCup::ExitOnInternalCommand when SIPp exits on an internal command. Calls may have been processed
34
60
  # @raises SippyCup::NoCallsProcessed when SIPp exit normally, but has processed no calls
@@ -38,15 +64,10 @@ module SippyCup
38
64
  #
39
65
  # @return Boolean true if execution succeeded without any failed calls, false otherwise
40
66
  #
41
- def run
42
- @input_files = @scenario.to_tmpfiles
43
-
44
- @logger.info "Preparing to run SIPp command: #{command}"
45
-
46
- exit_status, stderr_buffer = execute_with_redirected_streams
47
-
48
- final_result = process_exit_status exit_status, stderr_buffer
49
-
67
+ def wait
68
+ exit_status = Process.wait2 @sipp_pid.to_i
69
+ @rd.close if @rd
70
+ final_result = process_exit_status exit_status, @stderr_buffer
50
71
  if final_result
51
72
  @logger.info "Test completed successfully!"
52
73
  else
@@ -59,21 +80,11 @@ module SippyCup
59
80
  cleanup_input_files
60
81
  end
61
82
 
62
- #
63
- # Tries to stop SIPp by killing the target PID
64
- #
65
- # @raises Errno::ESRCH when the PID does not correspond to a known process
66
- # @raises Errno::EPERM when the process referenced by the PID cannot be killed
67
- #
68
- def stop
69
- Process.kill "KILL", @sipp_pid if @sipp_pid
70
- end
71
-
72
83
  private
73
84
 
74
85
  def command
75
86
  @command ||= begin
76
- command = "sudo sipp"
87
+ command = "sudo $(which sipp)"
77
88
  command_options.each_pair do |key, value|
78
89
  command << (value ? " -#{key} #{value}" : " -#{key}")
79
90
  end
@@ -86,10 +97,10 @@ module SippyCup
86
97
  i: @scenario_options[:source],
87
98
  p: @scenario_options[:source_port] || '8836',
88
99
  sf: @input_files[:scenario].path,
89
- l: @scenario_options[:max_concurrent],
90
- m: @scenario_options[:number_of_calls],
91
- r: @scenario_options[:calls_per_second],
92
- s: @scenario_options[:from_user] || '1'
100
+ l: @scenario_options[:max_concurrent] || 5,
101
+ m: @scenario_options[:number_of_calls] || 10,
102
+ r: @scenario_options[:calls_per_second] || 10,
103
+ s: @scenario_options[:to_user] || '1'
93
104
  }
94
105
 
95
106
  options[:mp] = @scenario_options[:media_port] if @scenario_options[:media_port]
@@ -108,31 +119,27 @@ module SippyCup
108
119
  options[:inf] = @scenario_options[:scenario_variables]
109
120
  end
110
121
 
122
+ options.merge! @scenario_options[:options] if @scenario_options[:options]
123
+
111
124
  options
112
125
  end
113
126
 
114
127
  def execute_with_redirected_streams
115
- rd, wr = IO.pipe
128
+ @rd, wr = IO.pipe
116
129
  stdout_target = @options[:full_sipp_output] ? $stdout : '/dev/null'
117
130
 
118
131
  @sipp_pid = spawn command, err: wr, out: stdout_target
119
132
 
120
- stderr_buffer = String.new
133
+ @stderr_buffer = String.new
121
134
 
122
135
  Thread.new do
123
136
  wr.close
124
- until rd.eof?
125
- buffer = rd.readpartial(1024).strip
126
- stderr_buffer += buffer
137
+ until @rd.eof?
138
+ buffer = @rd.readpartial(1024).strip
139
+ @stderr_buffer += buffer
127
140
  $stderr << buffer if @options[:full_sipp_output]
128
141
  end
129
142
  end
130
-
131
- exit_status = Process.wait2 @sipp_pid.to_i
132
-
133
- rd.close
134
-
135
- [exit_status, stderr_buffer]
136
143
  end
137
144
 
138
145
  def process_exit_status(process_status, error_message = nil)
@@ -156,7 +163,7 @@ module SippyCup
156
163
  end
157
164
 
158
165
  def cleanup_input_files
159
- @input_files.each_pair do |key, value|
166
+ @input_files.values.compact.each do |value|
160
167
  value.close
161
168
  value.unlink
162
169
  end
@@ -79,6 +79,7 @@ module SippyCup
79
79
  # @option options [String, Numeric] :source_port The source port to bind SIPp to (defaults to 8836).
80
80
  # @option options [String] :destination The target system at which to direct traffic.
81
81
  # @option options [String] :from_user The SIP user from which traffic should appear.
82
+ # @option options [String] :to_user The SIP user to send requests to.
82
83
  # @option options [Integer] :media_port The RTCP (media) port to bind to locally.
83
84
  # @option options [String, Numeric] :max_concurrent The maximum number of concurrent calls to execute.
84
85
  # @option options [String, Numeric] :number_of_calls The maximum number of calls to execute in the test run.
@@ -86,7 +87,9 @@ module SippyCup
86
87
  # @option options [String] :stats_file The path at which to dump statistics.
87
88
  # @option options [String, Numeric] :stats_interval The interval (in seconds) at which to dump statistics (defaults to 1s).
88
89
  # @option options [String] :transport_mode The transport mode over which to direct SIP traffic.
90
+ # @option options [String] :dtmf_mode The output DTMF mode, either rfc2833 (default) or info.
89
91
  # @option options [String] :scenario_variables A path to a CSV file of variables to be interpolated with the scenario at runtime.
92
+ # @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.
90
93
  # @option options [Array<String>] :steps A collection of steps
91
94
  #
92
95
  # @yield [scenario] Builder block to construct scenario
@@ -99,6 +102,8 @@ module SippyCup
99
102
  @filename = args[:filename] || name.downcase.gsub(/\W+/, '_')
100
103
  @filename = File.expand_path @filename, Dir.pwd
101
104
  @media = Media.new '127.0.0.255', 55555, '127.255.255.255', 5060
105
+ @message_variables = 0
106
+ @media_nodes = []
102
107
  @errors = []
103
108
 
104
109
  instance_eval &block if block_given?
@@ -198,7 +203,7 @@ a=fmtp:101 0-15
198
203
  # Sets an expectation for a SIP 100 message from the remote party
199
204
  #
200
205
  # @param [Hash] opts A set of options to modify the expectation
201
- # @option opts [true, false] :optional Wether or not receipt of the message is optional. Defaults to true.
206
+ # @option opts [true, false] :optional Whether or not receipt of the message is optional. Defaults to true.
202
207
  #
203
208
  def receive_trying(opts = {})
204
209
  handle_response 100, opts
@@ -209,7 +214,7 @@ a=fmtp:101 0-15
209
214
  # Sets an expectation for a SIP 180 message from the remote party
210
215
  #
211
216
  # @param [Hash] opts A set of options to modify the expectation
212
- # @option opts [true, false] :optional Wether or not receipt of the message is optional. Defaults to true.
217
+ # @option opts [true, false] :optional Whether or not receipt of the message is optional. Defaults to true.
213
218
  #
214
219
  def receive_ringing(opts = {})
215
220
  handle_response 180, opts
@@ -220,7 +225,7 @@ a=fmtp:101 0-15
220
225
  # Sets an expectation for a SIP 183 message from the remote party
221
226
  #
222
227
  # @param [Hash] opts A set of options to modify the expectation
223
- # @option opts [true, false] :optional Wether or not receipt of the message is optional. Defaults to true.
228
+ # @option opts [true, false] :optional Whether or not receipt of the message is optional. Defaults to true.
224
229
  #
225
230
  def receive_progress(opts = {})
226
231
  handle_response 183, opts
@@ -229,20 +234,30 @@ a=fmtp:101 0-15
229
234
 
230
235
  #
231
236
  # Sets an expectation for a SIP 200 message from the remote party
237
+ # as well as storing the record set and the response time duration
232
238
  #
233
239
  # @param [Hash] opts A set of options to modify the expectation
234
- # @option opts [true, false] :optional Wether or not receipt of the message is optional. Defaults to true.
240
+ # @option opts [true, false] :optional Whether or not receipt of the message is optional. Defaults to false.
235
241
  #
236
242
  def receive_answer(opts = {})
237
243
  options = {
238
- response: 200,
239
244
  rrs: true, # Record Record Set: Make the Route headers available via [route] later
240
245
  rtd: true # Response Time Duration: Record the response time
241
246
  }
242
247
 
243
- recv options.merge(opts)
248
+ receive_200 options.merge(opts)
244
249
  end
245
- alias :receive_200 :receive_answer
250
+
251
+ #
252
+ # Sets an expectation for a SIP 200 message from the remote party
253
+ #
254
+ # @param [Hash] opts A set of options to modify the expectation
255
+ # @option opts [true, false] :optional Whether or not receipt of the message is optional. Defaults to false.
256
+ #
257
+ def receive_200(opts = {})
258
+ recv({ response: 200 }.merge(opts))
259
+ end
260
+ alias :receive_200 :receive_ok
246
261
 
247
262
  #
248
263
  # Shortcut that sets expectations for optional SIP 100, 180 and 183, followed by a required 200.
@@ -250,9 +265,9 @@ a=fmtp:101 0-15
250
265
  # @param [Hash] opts A set of options to modify the expectations
251
266
  #
252
267
  def wait_for_answer(opts = {})
253
- receive_trying({optional: true}.merge opts)
254
- receive_ringing({optional: true}.merge opts)
255
- receive_progress({optional: true}.merge opts)
268
+ receive_trying opts
269
+ receive_ringing opts
270
+ receive_progress opts
256
271
  receive_answer opts
257
272
  end
258
273
 
@@ -267,7 +282,7 @@ a=fmtp:101 0-15
267
282
  ACK [next_url] SIP/2.0
268
283
  Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
269
284
  From: "#{@from_user}" <sip:#{@from_user}@[local_ip]>;tag=[call_number]
270
- [last_To:]
285
+ To: <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
271
286
  Call-ID: [call_id]
272
287
  CSeq: [cseq] ACK
273
288
  Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
@@ -307,10 +322,68 @@ Content-Length: 0
307
322
  digits.split('').each do |digit|
308
323
  raise ArgumentError, "Invalid DTMF digit requested: #{digit}" unless VALID_DTMF.include? digit
309
324
 
310
- @media << "dtmf:#{digit}"
311
- @media << "silence:#{delay}"
325
+ case @dtmf_mode
326
+ when :rfc2833
327
+ @media << "dtmf:#{digit}"
328
+ @media << "silence:#{delay}"
329
+ when :info
330
+ info = <<-INFO
331
+
332
+ INFO [next_url] SIP/2.0
333
+ Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
334
+ From: "#{@from_user}" <sip:#{@from_user}@[local_ip]>;tag=[call_number]
335
+ To: <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
336
+ Call-ID: [call_id]
337
+ CSeq: [cseq] INFO
338
+ Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
339
+ Max-Forwards: 100
340
+ User-Agent: #{USER_AGENT}
341
+ [routes]
342
+ Content-Length: [len]
343
+ Content-Type: application/dtmf-relay
344
+
345
+ Signal=#{digit}
346
+ Duration=#{delay}
347
+ INFO
348
+ send info
349
+ recv response: 200
350
+ pause delay
351
+ end
352
+ end
353
+
354
+ if @dtmf_mode == :rfc2833
355
+ pause delay * 2 * digits.size
312
356
  end
313
- pause delay * 2 * digits.size
357
+ end
358
+
359
+ #
360
+ # Expect to receive a MESSAGE message
361
+ #
362
+ # @param [String] regexp A regular expression (as a String) to match the message body against
363
+ #
364
+ def receive_message(regexp = nil)
365
+ recv = Nokogiri::XML::Node.new 'recv', doc
366
+ recv['request'] = 'MESSAGE'
367
+ scenario_node << recv
368
+
369
+ if regexp
370
+ action = Nokogiri::XML::Node.new 'action', doc
371
+ ereg = Nokogiri::XML::Node.new 'ereg', doc
372
+ ref = Nokogiri::XML::Node.new 'Reference', doc
373
+
374
+ ereg['regexp'] = regexp
375
+ ereg['search_in'] = 'body'
376
+ ereg['check_it'] = true
377
+
378
+ var = "message_#{@message_variables += 1}"
379
+ ereg['assign_to'] = ref['variables'] = var
380
+
381
+ action << ereg
382
+ recv << action
383
+ scenario_node << ref
384
+ end
385
+
386
+ okay
314
387
  end
315
388
 
316
389
  #
@@ -322,10 +395,10 @@ Content-Length: 0
322
395
  msg = <<-MSG
323
396
 
324
397
  BYE [next_url] SIP/2.0
325
- [last_Via:]
398
+ Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
326
399
  From: "#{@from_user}" <sip:#{@from_user}@[local_ip]>;tag=[call_number]
327
- [last_To:]
328
- [last_Call-ID]
400
+ To: <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
401
+ Call-ID: [call_id]
329
402
  CSeq: [cseq] BYE
330
403
  Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
331
404
  Max-Forwards: 100
@@ -346,11 +419,11 @@ Content-Length: 0
346
419
  end
347
420
 
348
421
  #
349
- # Acknowledge a received BYE message
422
+ # Acknowledge the last request
350
423
  #
351
424
  # @param [Hash] opts A set of options to modify the message parameters
352
425
  #
353
- def ack_bye(opts = {})
426
+ def okay(opts = {})
354
427
  msg = <<-ACK
355
428
 
356
429
  SIP/2.0 200 OK
@@ -367,6 +440,7 @@ Content-Length: 0
367
440
  ACK
368
441
  send msg, opts
369
442
  end
443
+ alias :ack_bye :okay
370
444
 
371
445
  #
372
446
  # Shortcut to set an expectation for a BYE and acknowledge it when received
@@ -382,14 +456,30 @@ Content-Length: 0
382
456
  # Dump the scenario to a SIPp XML string
383
457
  #
384
458
  # @return [String] the SIPp XML scenario
385
- def to_xml
386
- doc.to_xml
459
+ def to_xml(options = {})
460
+ pcap_path = options[:pcap_path]
461
+ docdup = doc.dup
462
+
463
+ # Not removing in reverse would most likely remove the wrong
464
+ # nodes because of changing indices.
465
+ @media_nodes.reverse.each do |nop|
466
+ nopdup = docdup.xpath(nop.path)
467
+
468
+ if pcap_path.nil? or @media.empty?
469
+ nopdup.remove
470
+ else
471
+ exec = nopdup.xpath("./action/exec").first
472
+ exec['play_pcap_audio'] = pcap_path
473
+ end
474
+ end
475
+
476
+ docdup.to_xml
387
477
  end
388
478
 
389
479
  #
390
480
  # Compile the scenario and its media to disk
391
481
  #
392
- # Writes the SIPp scenario file to disk at {filename}.xml, and the PCAP media to {filename}.pcap.
482
+ # Writes the SIPp scenario file to disk at {filename}.xml, and the PCAP media to {filename}.pcap if applicable.
393
483
  # {filename} is taken from the :filename option when creating the scenario, or falls back to a down-snake-cased version of the scenario name.
394
484
  #
395
485
  # @return [String] the path to the resulting scenario file
@@ -403,22 +493,24 @@ Content-Length: 0
403
493
  # scenario.compile! # Leaves files at test_scenario.xml and test_scenario.pcap
404
494
  #
405
495
  def compile!
496
+ unless @media.empty?
497
+ print "Compiling media to #{@filename}.pcap..."
498
+ compile_media.to_file filename: "#{@filename}.pcap"
499
+ puts "done."
500
+ end
501
+
406
502
  scenario_filename = "#{@filename}.xml"
407
503
  print "Compiling scenario to #{scenario_filename}..."
408
504
  File.open scenario_filename, 'w' do |file|
409
- file.write doc.to_xml
505
+ file.write to_xml(:pcap_path => "#{@filename}.pcap")
410
506
  end
411
507
  puts "done."
412
508
 
413
- print "Compiling media to #{@filename}.pcap..."
414
- compile_media.to_file filename: "#{@filename}.pcap"
415
- puts "done."
416
-
417
509
  scenario_filename
418
510
  end
419
511
 
420
512
  #
421
- # Write compiled Scenario XML and PCAP media to tempfiles.
513
+ # Write compiled Scenario XML and PCAP media (if applicable) to tempfiles.
422
514
  #
423
515
  # 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.
424
516
  #
@@ -427,14 +519,16 @@ Content-Length: 0
427
519
  # @see http://www.ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/Tempfile.html
428
520
  #
429
521
  def to_tmpfiles
522
+ unless @media.empty?
523
+ media_file = Tempfile.new 'media'
524
+ media_file.write compile_media.to_s
525
+ media_file.rewind
526
+ end
527
+
430
528
  scenario_file = Tempfile.new 'scenario'
431
- scenario_file.write to_xml
529
+ scenario_file.write to_xml(:pcap_path => media_file.try(:path))
432
530
  scenario_file.rewind
433
531
 
434
- media_file = Tempfile.new 'media'
435
- media_file.write compile_media.to_s
436
- media_file.rewind
437
-
438
532
  {scenario: scenario_file, media: media_file}
439
533
  end
440
534
 
@@ -452,19 +546,29 @@ Content-Length: 0
452
546
  def doc
453
547
  @doc ||= begin
454
548
  Nokogiri::XML::Builder.new do |xml|
455
- xml.scenario name: @scenario_options[:name]
549
+ xml.scenario name: @scenario_options[:name] do
550
+ @scenario_node = xml.parent
551
+ end
456
552
  end.doc
457
553
  end
458
554
  end
459
555
 
460
556
  def scenario_node
461
- @scenario_node = doc.xpath('//scenario').first
557
+ doc
558
+ @scenario_node
462
559
  end
463
560
 
464
561
  def parse_args(args)
465
562
  raise ArgumentError, "Must include source IP:PORT" unless args.has_key? :source
466
563
  raise ArgumentError, "Must include destination IP:PORT" unless args.has_key? :destination
467
564
 
565
+ if args[:dtmf_mode]
566
+ @dtmf_mode = args[:dtmf_mode].to_sym
567
+ raise ArgumentError, "dtmf_mode must be rfc2833 or info" unless [:rfc2833, :info].include?(@dtmf_mode)
568
+ else
569
+ @dtmf_mode = :rfc2833
570
+ end
571
+
468
572
  @from_addr, @from_port = args[:source].split ':'
469
573
  @to_addr, @to_port = args[:destination].split ':'
470
574
  @from_user = args[:from_user] || "sipp"
@@ -511,12 +615,13 @@ Content-Length: 0
511
615
  end
512
616
 
513
617
  def start_media
514
- nop = Nokogiri::XML::Node.new 'nop', doc
515
- action = Nokogiri::XML::Node.new 'action', doc
516
- nop << action
517
- exec = Nokogiri::XML::Node.new 'exec', doc
518
- exec['play_pcap_audio'] = "#{@filename}.pcap"
519
- action << exec
618
+ nop = doc.create_element('nop') { |nop|
619
+ nop << doc.create_element('action') { |action|
620
+ action << doc.create_element('exec')
621
+ }
622
+ }
623
+
624
+ @media_nodes << nop
520
625
  scenario_node << nop
521
626
  end
522
627
 
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module SippyCup
4
- VERSION = '0.3.0'
4
+ VERSION = '0.4.0'
5
5
  end
data/sippy_cup.gemspec CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |s|
21
21
  s.add_runtime_dependency 'packetfu'
22
22
  s.add_runtime_dependency 'nokogiri', ["~> 1.6.0"]
23
23
  s.add_runtime_dependency 'activesupport', ["> 3.0"]
24
- s.add_runtime_dependency 'psych', ["~> 2.0.0"] unless RUBY_PLATFORM == 'java'
24
+ s.add_runtime_dependency 'psych', ["~> 2.0.1"] unless RUBY_PLATFORM == 'java'
25
25
 
26
26
  s.add_development_dependency 'guard-rspec'
27
27
  s.add_development_dependency 'rspec', ["~> 2.11"]
@@ -13,6 +13,15 @@ describe SippyCup::Media do
13
13
  @media.sequence.should be_empty
14
14
  end
15
15
 
16
+ it 'should correctly report itself as empty' do
17
+ expect(@media.empty?).to be true
18
+ end
19
+
20
+ it 'should correctly report itself as non-empty' do
21
+ @media << 'silence:1000'
22
+ expect(@media.empty?).to be false
23
+ end
24
+
16
25
  it 'should append a valid action to the sequence list' do
17
26
  @media << 'silence:1000'
18
27
  @media.sequence.include?('silence:1000').should be true