sippy_cup 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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