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 +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
|