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 +5 -13
- data/.travis.yml +2 -0
- data/CHANGELOG.md +10 -2
- data/README.markdown +19 -5
- data/lib/sippy_cup/media.rb +4 -2
- data/lib/sippy_cup/runner.rb +43 -36
- data/lib/sippy_cup/scenario.rb +146 -41
- data/lib/sippy_cup/version.rb +1 -1
- data/sippy_cup.gemspec +1 -1
- data/spec/sippy_cup/media_spec.rb +9 -0
- data/spec/sippy_cup/runner_spec.rb +81 -20
- data/spec/sippy_cup/scenario_spec.rb +124 -45
- metadata +29 -30
- data/lib/sippy_cup/rtp_generator.rb +0 -19
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
NTU0NDIwOWUyZjc5MTRjMzkzOTQwZTNhZWZjNjhlNTMwZmM0N2NhYQ==
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 12ebcabfa7aeab3fd07ebd8ccf1a0f4cfb88b859
|
4
|
+
data.tar.gz: 917066d6774889765f8c19d40d694d85dc02c19a
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
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
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,13 @@
|
|
1
|
-
#
|
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.
|
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.
|
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>
|
188
|
-
<dd>SIP
|
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
|
|
data/lib/sippy_cup/media.rb
CHANGED
@@ -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
|
data/lib/sippy_cup/runner.rb
CHANGED
@@ -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
|
42
|
-
|
43
|
-
|
44
|
-
|
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[:
|
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.
|
166
|
+
@input_files.values.compact.each do |value|
|
160
167
|
value.close
|
161
168
|
value.unlink
|
162
169
|
end
|
data/lib/sippy_cup/scenario.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
-
|
248
|
+
receive_200 options.merge(opts)
|
244
249
|
end
|
245
|
-
|
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
|
254
|
-
receive_ringing
|
255
|
-
receive_progress
|
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
|
-
[
|
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
|
-
@
|
311
|
-
|
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
|
-
|
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
|
-
[
|
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
|
-
[
|
328
|
-
|
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
|
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
|
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
|
-
|
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
|
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
|
-
|
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 =
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
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
|
|
data/lib/sippy_cup/version.rb
CHANGED
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.
|
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
|