sippy_cup 0.4.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.travis.yml +0 -2
- data/CHANGELOG.md +16 -0
- data/Guardfile +1 -1
- data/README.markdown +35 -2
- data/bin/sippy_cup +9 -1
- data/examples/navigate_ivr.yml +14 -0
- data/examples/simple_call.yml +11 -0
- data/examples/wait_for_call.yml +15 -0
- data/lib/sippy_cup/runner.rb +46 -10
- data/lib/sippy_cup/scenario.rb +268 -54
- data/lib/sippy_cup/version.rb +1 -1
- data/sippy_cup.gemspec +1 -1
- data/spec/sippy_cup/runner_spec.rb +102 -12
- data/spec/sippy_cup/scenario_spec.rb +78 -83
- data/spec/spec_helper.rb +1 -1
- metadata +8 -7
- data/.ruby-version +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6dd7d94d947152635902fedf054429f24ffa2de8
|
4
|
+
data.tar.gz: 621cb762e6bbef801190e0aeb855c82eeb9ed064
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2423aa6cff4c235c5416b3f3fdbea27a973a79eff72fee91a56862386827eaa0a3d04bc260515d4031331623abbc2da0271589d7cb505380eb89c499434d2f34
|
7
|
+
data.tar.gz: a45bac4f0fbb3a98c5e40dcc8aa4761a49bf6b4d8f3452f8fed86f141456cc200148ab2c2739681a81abe53e07510259837f5f9a3dd23f5dd7fd74b15f061543
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,19 @@
|
|
1
|
+
# develop
|
2
|
+
|
3
|
+
# [0.5.0](https://github.com/mojolingo/sippy_cup/compare/v0.4.1...v0.5.0)
|
4
|
+
SYNTAX CHANGES!
|
5
|
+
This is a backward incompatible change. If upgrading from Sippy Cup 0.4.x please see the [documentation](http://mojolingo.github.io/sippy_cup/#available-scenario-steps) and the `Change` items below.
|
6
|
+
You will also need to compile the latest [SIPp from Github](https://github.com/sipp/sipp) to make use of all the features.
|
7
|
+
* Feature: Add support for saving screen and error reports to specified files
|
8
|
+
* Feature: Add support for UAS actions (waiting for an incoming call)
|
9
|
+
* Feature: Permit supplying a SIP advertise address that is different from the bind IP for NAT traversal purposes
|
10
|
+
* Feature: Add support for CallLengthRepartition and ResponseTimeRepartition tables
|
11
|
+
* Bugfix: Much improved support for sending a hangup from SIPp, rather than waiting for the far end to do it
|
12
|
+
* Bugfix: If scenario compilation fails on the CLI, explain why
|
13
|
+
* Change: Rework the `register` command so it works without any other expectations
|
14
|
+
* Change: `wait_for_answer` now includes `ack_answer`
|
15
|
+
* Documentation: Create `examples/` directory with example scenarios
|
16
|
+
|
1
17
|
# [0.4.1](https://github.com/bklang/sippy_cup/compare/v0.4.0...v0.4.1)
|
2
18
|
* Bugfix: Fix some Ruby 2 string encoding problems
|
3
19
|
* Bugfix: Fix backward alias of `respond_ok` with `respond_200`
|
data/Guardfile
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
guard 'rspec', :
|
1
|
+
guard 'rspec', cmd: 'bundle exec rspec --format documentation', :all_on_start => true, :all_after_pass => true do
|
2
2
|
watch(%r{^spec/.+_spec\.rb$})
|
3
3
|
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
4
4
|
watch('spec/spec_helper.rb') { "spec/" }
|
data/README.markdown
CHANGED
@@ -105,6 +105,8 @@ Password:
|
|
105
105
|
I, [2013-09-30T14:48:16.728712 #9883] INFO -- : Test completed successfully.
|
106
106
|
```
|
107
107
|
|
108
|
+
More examples are [available in the source repository](https://github.com/mojolingo/sippy_cup/tree/develop/examples).
|
109
|
+
|
108
110
|
### Example embedding SIPp in another Ruby process
|
109
111
|
|
110
112
|
```Ruby
|
@@ -135,17 +137,23 @@ The above code can be executed as a standalone Ruby script and the resulting sce
|
|
135
137
|
|
136
138
|
### Available Scenario Steps
|
137
139
|
|
138
|
-
Each command below can take [SIPp attributes](http://sipp.sourceforge.net/doc/reference.html) as optional arguments. For a full list of available steps, see the [API documentation](http://rubydoc.info/gems/sippy_cup/SippyCup/Scenario).
|
140
|
+
Each command below can take [SIPp attributes](http://sipp.sourceforge.net/doc/reference.html) as optional arguments. For a full list of available steps with arguments explained, see the [API documentation](http://rubydoc.info/gems/sippy_cup/SippyCup/Scenario).
|
139
141
|
|
140
142
|
* `sleep <seconds>` Wait a specified number of seconds
|
141
143
|
* `invite` Send a SIP INVITE to the specified target
|
144
|
+
* `receive_invite` Wait for an INVITE to be received
|
142
145
|
* `register <username> [password]` Register the specified user to the target with an optional password
|
146
|
+
* `send_trying` Send a `100 Trying` provisional response
|
143
147
|
* `receive_trying` Expect to receive a `100 Trying` response from the target
|
148
|
+
* `send_ringing` Send a `180 Ringing` provisional response
|
144
149
|
* `receive_ringing` Expect to receive a `180 Ringing` response from the target
|
145
150
|
* `receive_progress` Expect to receive a `183 Progress` response from the target
|
151
|
+
* `send_answer` Send a `200 Ok` response to an INVITE (answer the call)
|
146
152
|
* `receive_answer` Expect to receive a `200 OK` (answering the call) response from the target
|
153
|
+
* `answer` Convenient shortcut for `send_answer; receive_ack`
|
147
154
|
* `wait_for_answer` Convenient shortcut for `receive_trying; receive_ringing; receive_progress; receive_answer`, with all but the `answer` marked as optional
|
148
155
|
* `ack_answer` Send an `ACK` in response to a `200 OK`
|
156
|
+
* `receive_ack` Expect to receive an `ACK`
|
149
157
|
* `send_digits <string>` Send a DTMF string. May send one or many digits, including `0-9`, `*`, `#`, and `A-D`
|
150
158
|
* `receive_ok` Expect to receive a `200 OK`
|
151
159
|
* `receive_message [regex]` Expect to receive a SIP MESSAGE, optionally matching a regex
|
@@ -153,6 +161,9 @@ Each command below can take [SIPp attributes](http://sipp.sourceforge.net/doc/re
|
|
153
161
|
* `receive_bye` Expect to receive a `BYE` from the target
|
154
162
|
* `ack_bye` Send a `200 OK` response to a `BYE`
|
155
163
|
* `wait_for_hangup` Convenient shortcut for `receive_bye; ack_bye`
|
164
|
+
* `hangup` Convenient shortcut for `send_bye; receive_ok`
|
165
|
+
* `call_length_repartition` Creates a histogram table of individual call lengths in milliseconds between min length and max length, at the specified interval
|
166
|
+
* `response_time_repartition` Creates a histogram table of individual SIP request response times in milliseconds between min length and max length, at the specified interval
|
156
167
|
|
157
168
|
### Alternate Output File Path
|
158
169
|
|
@@ -179,6 +190,7 @@ This will create the files `somewhere.xml` and `somewhere.pcap` in the `/path/to
|
|
179
190
|
### Customizing the Test Run
|
180
191
|
|
181
192
|
Each parameter has an impact on the test, and may either be changed once the XML file is generated or specified in the options hash for `SippyCup::Scenario.new`. In addition to the default parameters, some additional parameters can be set:
|
193
|
+
|
182
194
|
<dl>
|
183
195
|
<dt>stats_file</dt>
|
184
196
|
<dd>Path to a file where call statistics will be stored in a CSV format, defaults to not storing stats</dd>
|
@@ -198,6 +210,12 @@ Each parameter has an impact on the test, and may either be changed once the XML
|
|
198
210
|
<dt>full_sipp_output</dt>
|
199
211
|
<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>
|
200
212
|
|
213
|
+
<dt>summary_report_file</dt>
|
214
|
+
<dd>Write a summary of the SIPp run to the specified file. This summary is the output from the SIPp `-trace_screen` command. Requires a development build of SIPp; see https://github.com/SIPp/sipp/pull/106.</dd>
|
215
|
+
|
216
|
+
<dt>errors_report_file</dt>
|
217
|
+
<dd>Record SIPp's errors to the specified file. This report is the output from the SIPp `-trace_err` command.</dd>
|
218
|
+
|
201
219
|
<dt>options</dt>
|
202
220
|
<dd>A string of SIPp command line options included with the SIPp run.</dd>
|
203
221
|
|
@@ -205,10 +223,25 @@ Each parameter has an impact on the test, and may either be changed once the XML
|
|
205
223
|
<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>
|
206
224
|
|
207
225
|
<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
|
226
|
+
<dd>Specify the mechanism by which DTMF is signaled. Valid options are `rfc2833` for within the RTP media, or `info` for SIP INFO. Default: rfc2833</dd>
|
209
227
|
|
210
228
|
<dt>scenario_variables</dt>
|
211
229
|
<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>
|
230
|
+
|
231
|
+
<dt>concurrent_max</dt>
|
232
|
+
<dd>The maximum number of calls permitted to be active at any given time. When this limit is reached, SIPp will slow down or stop sending new calls until there it falls below the limit. Default: 5</dd>
|
233
|
+
|
234
|
+
<dt>calls_per_second</dt>
|
235
|
+
<dd>The rate at which new calls should be created. Note that SIPp will automatically adjust this downward to stay at or beneath the maximum number of concurrent calls (`concurrent_max`). Default: 10</dt>
|
236
|
+
|
237
|
+
<dt>calls_per_second_incr</dt>
|
238
|
+
<dd>When used with `calls_per_second_max`, tells SIPp the amount by which calls-per-second should be incremented. CPS rate is adjusted each `stats_interval`. Default: 1.</dd>
|
239
|
+
|
240
|
+
<dt>calls_per_second_max</dt>
|
241
|
+
<dd>The maximum rate of calls-per-second. If unset, the CPS rate will remain at the level set by `calls_per_second`.</dd>
|
242
|
+
|
243
|
+
<dt>advertise_address</dt>
|
244
|
+
<dd>The IP address to advertise in SIP and SDP if different from the bind IP (defaults to the bind IP).</dd>
|
212
245
|
</dl>
|
213
246
|
|
214
247
|
### Additional SIPp Scenario Attributes
|
data/bin/sippy_cup
CHANGED
@@ -60,7 +60,15 @@ unless compile || run
|
|
60
60
|
end
|
61
61
|
|
62
62
|
scenario = SippyCup::Scenario.from_manifest File.read(manifest_path), input_filename: manifest_path
|
63
|
-
scenario.
|
63
|
+
if scenario.valid?
|
64
|
+
scenario.compile! if compile
|
65
|
+
else
|
66
|
+
$stderr.puts "Errors encountered while building the scenario!"
|
67
|
+
puts "Step\tError Message"
|
68
|
+
scenario.errors.each do |error|
|
69
|
+
puts "#{error[:step]}\t#{error[:message]}"
|
70
|
+
end
|
71
|
+
end
|
64
72
|
|
65
73
|
if run
|
66
74
|
runner = SippyCup::Runner.new scenario
|
@@ -0,0 +1,14 @@
|
|
1
|
+
source: 192.0.2.15
|
2
|
+
destination: 192.0.2.200
|
3
|
+
max_concurrent: 10
|
4
|
+
calls_per_second: 5
|
5
|
+
number_of_calls: 20
|
6
|
+
steps:
|
7
|
+
- invite
|
8
|
+
- wait_for_answer
|
9
|
+
- ack_answer
|
10
|
+
- sleep 3
|
11
|
+
- send_digits '3125551234'
|
12
|
+
- sleep 5
|
13
|
+
- send_digits '#'
|
14
|
+
- wait_for_hangup
|
data/lib/sippy_cup/runner.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require 'logger'
|
3
|
+
require 'fileutils'
|
3
4
|
|
4
5
|
#
|
5
6
|
# Service object to oversee the execution of a Scenario
|
@@ -67,7 +68,8 @@ module SippyCup
|
|
67
68
|
#
|
68
69
|
def wait
|
69
70
|
exit_status = Process.wait2 @sipp_pid.to_i
|
70
|
-
@
|
71
|
+
@err_rd.close if @err_rd
|
72
|
+
@stdout_rd.close if @stdout_rd
|
71
73
|
final_result = process_exit_status exit_status, @stderr_buffer
|
72
74
|
if final_result
|
73
75
|
@logger.info "Test completed successfully!"
|
@@ -95,23 +97,39 @@ module SippyCup
|
|
95
97
|
|
96
98
|
def command_options
|
97
99
|
options = {
|
98
|
-
i: @scenario_options[:source],
|
99
100
|
p: @scenario_options[:source_port] || '8836',
|
100
101
|
sf: @input_files[:scenario].path,
|
101
|
-
l: @scenario_options[:max_concurrent] || 5,
|
102
|
+
l: @scenario_options[:concurrent_max] || @scenario_options[:max_concurrent] || 5,
|
102
103
|
m: @scenario_options[:number_of_calls] || 10,
|
103
104
|
r: @scenario_options[:calls_per_second] || 10,
|
104
105
|
s: @scenario_options[:to_user] || '1'
|
105
106
|
}
|
106
107
|
|
108
|
+
options[:i] = @scenario_options[:source] if @scenario_options[:source]
|
107
109
|
options[:mp] = @scenario_options[:media_port] if @scenario_options[:media_port]
|
108
110
|
|
111
|
+
if @scenario_options[:calls_per_second_max]
|
112
|
+
options[:no_rate_quit] = nil
|
113
|
+
options[:rate_max] = @scenario_options[:calls_per_second_max]
|
114
|
+
options[:rate_increase] = @scenario_options[:calls_per_second_incr] || 1
|
115
|
+
end
|
116
|
+
|
109
117
|
if @scenario_options[:stats_file]
|
110
118
|
options[:trace_stat] = nil
|
111
119
|
options[:stf] = @scenario_options[:stats_file]
|
112
120
|
options[:fd] = @scenario_options[:stats_interval] || 1
|
113
121
|
end
|
114
122
|
|
123
|
+
if @scenario_options[:summary_report_file]
|
124
|
+
options[:trace_screen] = nil
|
125
|
+
options[:screen_file] = @scenario_options[:summary_report_file]
|
126
|
+
end
|
127
|
+
|
128
|
+
if @scenario_options[:errors_report_file]
|
129
|
+
options[:trace_err] = nil
|
130
|
+
options[:error_file] = @scenario_options[:errors_report_file]
|
131
|
+
end
|
132
|
+
|
115
133
|
if @scenario_options[:transport_mode]
|
116
134
|
options[:t] = @scenario_options[:transport_mode]
|
117
135
|
end
|
@@ -126,21 +144,39 @@ module SippyCup
|
|
126
144
|
end
|
127
145
|
|
128
146
|
def execute_with_redirected_streams
|
129
|
-
@
|
130
|
-
stdout_target = @options[:full_sipp_output]
|
147
|
+
@err_rd, err_wr = IO.pipe
|
148
|
+
stdout_target = if @options[:full_sipp_output]
|
149
|
+
@stdout_rd, stdout_wr = IO.pipe
|
150
|
+
stdout_wr
|
151
|
+
else
|
152
|
+
'/dev/null'
|
153
|
+
end
|
131
154
|
|
132
|
-
@sipp_pid = spawn command, err:
|
155
|
+
@sipp_pid = spawn command, err: err_wr, out: stdout_target
|
133
156
|
|
134
157
|
@stderr_buffer = String.new
|
135
158
|
|
136
159
|
Thread.new do
|
137
|
-
|
138
|
-
until @
|
139
|
-
buffer = @
|
160
|
+
err_wr.close
|
161
|
+
until @err_rd.eof?
|
162
|
+
buffer = @err_rd.readpartial(1024).strip
|
140
163
|
@stderr_buffer += buffer
|
141
164
|
$stderr << buffer if @options[:full_sipp_output]
|
142
165
|
end
|
143
166
|
end
|
167
|
+
|
168
|
+
if @stdout_rd
|
169
|
+
@stdout_buffer = String.new
|
170
|
+
|
171
|
+
Thread.new do
|
172
|
+
stdout_wr.close
|
173
|
+
until @stdout_rd.eof?
|
174
|
+
buffer = @stdout_rd.readpartial(1024).strip
|
175
|
+
@stdout_buffer += buffer
|
176
|
+
$stdout << buffer
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
144
180
|
end
|
145
181
|
|
146
182
|
def process_exit_status(process_status, error_message = nil)
|
@@ -167,7 +203,7 @@ module SippyCup
|
|
167
203
|
@input_files.values.compact.each do |value|
|
168
204
|
value.close
|
169
205
|
value.unlink
|
170
|
-
end
|
206
|
+
end if @input_files
|
171
207
|
end
|
172
208
|
end
|
173
209
|
|
data/lib/sippy_cup/scenario.rb
CHANGED
@@ -3,6 +3,7 @@ require 'nokogiri'
|
|
3
3
|
require 'psych'
|
4
4
|
require 'active_support/core_ext/hash'
|
5
5
|
require 'tempfile'
|
6
|
+
require 'set'
|
6
7
|
|
7
8
|
module SippyCup
|
8
9
|
#
|
@@ -12,6 +13,7 @@ module SippyCup
|
|
12
13
|
USER_AGENT = "SIPp/sippy_cup"
|
13
14
|
VALID_DTMF = %w{0 1 2 3 4 5 6 7 8 9 0 * # A B C D}.freeze
|
14
15
|
MSEC = 1_000
|
16
|
+
DEFAULT_RETRANS = 500
|
15
17
|
|
16
18
|
#
|
17
19
|
# Build a scenario based on either a manifest string or a file handle. Manifests are supplied in YAML format.
|
@@ -79,6 +81,7 @@ module SippyCup
|
|
79
81
|
# @option options [String] :source The source IP/hostname with which to invoke SIPp.
|
80
82
|
# @option options [String, Numeric] :source_port The source port to bind SIPp to (defaults to 8836).
|
81
83
|
# @option options [String] :destination The target system at which to direct traffic.
|
84
|
+
# @option options [String] :advertise_address The IP address to advertise in SIP and SDP if different from the bind IP (defaults to the bind IP).
|
82
85
|
# @option options [String] :from_user The SIP user from which traffic should appear.
|
83
86
|
# @option options [String] :to_user The SIP user to send requests to.
|
84
87
|
# @option options [Integer] :media_port The RTCP (media) port to bind to locally.
|
@@ -102,10 +105,13 @@ module SippyCup
|
|
102
105
|
@scenario_options = args.merge name: name
|
103
106
|
@filename = args[:filename] || name.downcase.gsub(/\W+/, '_')
|
104
107
|
@filename = File.expand_path @filename, Dir.pwd
|
105
|
-
@media =
|
108
|
+
@media = nil
|
106
109
|
@message_variables = 0
|
110
|
+
# Reference variables don't generate warnings/errors if unused in the scenario
|
111
|
+
@reference_variables = Set.new
|
107
112
|
@media_nodes = []
|
108
113
|
@errors = []
|
114
|
+
@adv_ip = args[:advertise_address] || "[local_ip]"
|
109
115
|
|
110
116
|
instance_eval &block if block_given?
|
111
117
|
end
|
@@ -124,11 +130,10 @@ module SippyCup
|
|
124
130
|
raise ArgumentError, "Must provide scenario steps" unless steps
|
125
131
|
steps.each_with_index do |step, index|
|
126
132
|
begin
|
127
|
-
instruction,
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
self.__send__ instruction, arg
|
133
|
+
instruction, args = step.split ' ', 2
|
134
|
+
args = split_quoted_string args
|
135
|
+
if args && !args.empty?
|
136
|
+
self.__send__ instruction, *args
|
132
137
|
else
|
133
138
|
self.__send__ instruction
|
134
139
|
end
|
@@ -149,22 +154,24 @@ module SippyCup
|
|
149
154
|
opts[:retrans] ||= 500
|
150
155
|
# FIXME: The DTMF mapping (101) is hard-coded. It would be better if we could
|
151
156
|
# get this from the DTMF payload generator
|
157
|
+
from_addr = "#{@from_user}@#{@adv_ip}:[local_port]"
|
158
|
+
to_addr = "[service]@[remote_ip]:[remote_port]"
|
152
159
|
msg = <<-MSG
|
153
160
|
|
154
|
-
INVITE sip
|
155
|
-
Via: SIP/2.0/[transport]
|
156
|
-
From: "#{@from_user}" <sip:#{
|
157
|
-
To: <sip
|
161
|
+
INVITE sip:#{to_addr} SIP/2.0
|
162
|
+
Via: SIP/2.0/[transport] #{@adv_ip}:[local_port];branch=[branch]
|
163
|
+
From: "#{@from_user}" <sip:#{from_addr}>;tag=[call_number]
|
164
|
+
To: <sip:#{to_addr}>
|
158
165
|
Call-ID: [call_id]
|
159
166
|
CSeq: [cseq] INVITE
|
160
|
-
Contact: <sip:#{
|
167
|
+
Contact: <sip:#{from_addr};transport=[transport]>
|
161
168
|
Max-Forwards: 100
|
162
169
|
User-Agent: #{USER_AGENT}
|
163
170
|
Content-Type: application/sdp
|
164
171
|
Content-Length: [len]
|
165
172
|
#{opts.has_key?(:headers) ? opts.delete(:headers).sub(/\n*\Z/, "\n") : ''}
|
166
173
|
v=0
|
167
|
-
o=user1 53655765 2353687637 IN IP[local_ip_type]
|
174
|
+
o=user1 53655765 2353687637 IN IP[local_ip_type] #{@adv_ip}
|
168
175
|
s=-
|
169
176
|
c=IN IP[media_ip_type] [media_ip]
|
170
177
|
t=0 0
|
@@ -173,7 +180,24 @@ a=rtpmap:0 PCMU/8000
|
|
173
180
|
a=rtpmap:101 telephone-event/8000
|
174
181
|
a=fmtp:101 0-15
|
175
182
|
MSG
|
176
|
-
send msg, opts
|
183
|
+
send msg, opts do |send|
|
184
|
+
send << doc.create_element('action') do |action|
|
185
|
+
action << doc.create_element('assignstr') do |assignstr|
|
186
|
+
assignstr['assign_to'] = "remote_addr"
|
187
|
+
assignstr['value'] = to_addr
|
188
|
+
end
|
189
|
+
action << doc.create_element('assignstr') do |assignstr|
|
190
|
+
assignstr['assign_to'] = "local_addr"
|
191
|
+
assignstr['value'] = from_addr
|
192
|
+
end
|
193
|
+
action << doc.create_element('assignstr') do |assignstr|
|
194
|
+
assignstr['assign_to'] = "call_addr"
|
195
|
+
assignstr['value'] = to_addr
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
# These variables will only be used if we initiate a hangup
|
200
|
+
@reference_variables += %w(remote_addr local_addr call_addr)
|
177
201
|
end
|
178
202
|
|
179
203
|
#
|
@@ -190,16 +214,142 @@ a=fmtp:101 0-15
|
|
190
214
|
# s.register 'frank'
|
191
215
|
#
|
192
216
|
def register(user, password = nil, opts = {})
|
193
|
-
|
217
|
+
send_opts = opts.dup
|
218
|
+
send_opts[:retrans] ||= DEFAULT_RETRANS
|
194
219
|
user, domain = parse_user user
|
195
|
-
|
196
|
-
|
220
|
+
if password
|
221
|
+
send register_message(domain, user), send_opts
|
222
|
+
recv opts.merge(response: 401, auth: true, optional: false)
|
223
|
+
send register_auth(domain, user, password), send_opts
|
224
|
+
receive_ok opts.merge(optional: false)
|
197
225
|
else
|
198
|
-
register_message
|
226
|
+
send register_message(domain, user), send_opts
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
#
|
231
|
+
# Expect to receive a SIP INVITE
|
232
|
+
#
|
233
|
+
# @param [Hash] opts A set of options containing SIPp <recv> element attributes
|
234
|
+
#
|
235
|
+
def receive_invite(opts = {})
|
236
|
+
recv(opts.merge(request: 'INVITE', rrs: true)) do |recv|
|
237
|
+
action = doc.create_element('action') do |action|
|
238
|
+
action << doc.create_element('ereg') do |ereg|
|
239
|
+
ereg['regexp'] = '<sip:(.*)>.*;tag=([^;]*)'
|
240
|
+
ereg['search_in'] = 'hdr'
|
241
|
+
ereg['header'] = 'From:'
|
242
|
+
ereg['assign_to'] = 'dummy,remote_addr,remote_tag'
|
243
|
+
end
|
244
|
+
action << doc.create_element('ereg') do |ereg|
|
245
|
+
ereg['regexp'] = '<sip:(.*)>'
|
246
|
+
ereg['search_in'] = 'hdr'
|
247
|
+
ereg['header'] = 'To:'
|
248
|
+
ereg['assign_to'] = 'dummy,local_addr'
|
249
|
+
end
|
250
|
+
action << doc.create_element('assignstr') do |assignstr|
|
251
|
+
assignstr['assign_to'] = "call_addr"
|
252
|
+
assignstr['value'] = "[$local_addr]"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
recv << action
|
199
256
|
end
|
257
|
+
# These variables (except dummy) will only be used if we initiate a hangup
|
258
|
+
@reference_variables += %w(dummy remote_addr remote_tag local_addr call_addr)
|
259
|
+
end
|
260
|
+
alias :wait_for_call :receive_invite
|
261
|
+
|
262
|
+
#
|
263
|
+
# Send a "100 Trying" response
|
264
|
+
#
|
265
|
+
# @param [Hash] opts A set of options containing SIPp <recv> element attributes
|
266
|
+
#
|
267
|
+
def send_trying(opts = {})
|
268
|
+
msg = <<-MSG
|
269
|
+
|
270
|
+
SIP/2.0 100 Trying
|
271
|
+
[last_Via:]
|
272
|
+
From: <sip:[$remote_addr]>;tag=[$remote_tag]
|
273
|
+
To: <sip:[$local_addr]>;tag=[call_number]
|
274
|
+
[last_Call-ID:]
|
275
|
+
[last_CSeq:]
|
276
|
+
Server: #{USER_AGENT}
|
277
|
+
Contact: <sip:[$local_addr];transport=[transport]>
|
278
|
+
Content-Length: 0
|
279
|
+
MSG
|
280
|
+
send msg, opts
|
281
|
+
end
|
282
|
+
alias :send_100 :send_trying
|
283
|
+
|
284
|
+
#
|
285
|
+
# Send a "180 Ringing" response
|
286
|
+
#
|
287
|
+
# @param [Hash] opts A set of options containing SIPp <recv> element attributes
|
288
|
+
#
|
289
|
+
def send_ringing(opts = {})
|
290
|
+
msg = <<-MSG
|
291
|
+
|
292
|
+
SIP/2.0 180 Ringing
|
293
|
+
[last_Via:]
|
294
|
+
From: <sip:[$remote_addr]>;tag=[$remote_tag]
|
295
|
+
To: <sip:[$local_addr]>;tag=[call_number]
|
296
|
+
[last_Call-ID:]
|
297
|
+
[last_CSeq:]
|
298
|
+
Server: #{USER_AGENT}
|
299
|
+
Contact: <sip:[$local_addr];transport=[transport]>
|
300
|
+
Content-Length: 0
|
301
|
+
MSG
|
302
|
+
send msg, opts
|
303
|
+
end
|
304
|
+
alias :send_180 :send_ringing
|
305
|
+
|
306
|
+
#
|
307
|
+
# Answer an incoming call
|
308
|
+
#
|
309
|
+
# @param [Hash] opts A set of options containing SIPp <send> element attributes
|
310
|
+
#
|
311
|
+
def send_answer(opts = {})
|
312
|
+
opts[:retrans] ||= DEFAULT_RETRANS
|
313
|
+
msg = <<-MSG
|
314
|
+
|
315
|
+
SIP/2.0 200 Ok
|
316
|
+
[last_Via:]
|
317
|
+
From: <sip:[$remote_addr]>;tag=[$remote_tag]
|
318
|
+
To: <sip:[$local_addr]>;tag=[call_number]
|
319
|
+
[last_Call-ID:]
|
320
|
+
[last_CSeq:]
|
321
|
+
Server: #{USER_AGENT}
|
322
|
+
Contact: <sip:[$local_addr];transport=[transport]>
|
323
|
+
Content-Type: application/sdp
|
324
|
+
[routes]
|
325
|
+
Content-Length: [len]
|
326
|
+
|
327
|
+
v=0
|
328
|
+
o=user1 53655765 2353687637 IN IP[local_ip_type] #{@adv_ip}
|
329
|
+
s=-
|
330
|
+
c=IN IP[media_ip_type] [media_ip]
|
331
|
+
t=0 0
|
332
|
+
m=audio [media_port] RTP/AVP 0
|
333
|
+
a=rtpmap:0 PCMU/8000
|
334
|
+
MSG
|
335
|
+
start_media
|
200
336
|
send msg, opts
|
201
337
|
end
|
202
338
|
|
339
|
+
#
|
340
|
+
# Helper method to answer an INVITE and expect the ACK
|
341
|
+
#
|
342
|
+
# @param [Hash] opts A set of options containing SIPp element attributes - will be passed to both the <send> and <recv> elements
|
343
|
+
#
|
344
|
+
def answer(opts = {})
|
345
|
+
send_answer opts
|
346
|
+
receive_ack opts
|
347
|
+
end
|
348
|
+
|
349
|
+
def receive_ack(opts = {})
|
350
|
+
recv opts.merge request: 'ACK'
|
351
|
+
end
|
352
|
+
|
203
353
|
#
|
204
354
|
# Sets an expectation for a SIP 100 message from the remote party
|
205
355
|
#
|
@@ -242,11 +392,22 @@ a=fmtp:101 0-15
|
|
242
392
|
#
|
243
393
|
def receive_answer(opts = {})
|
244
394
|
options = {
|
245
|
-
rrs: true, # Record Record Set: Make the Route headers available via [
|
395
|
+
rrs: true, # Record Record Set: Make the Route headers available via [routes] later
|
246
396
|
rtd: true # Response Time Duration: Record the response time
|
247
397
|
}
|
248
398
|
|
249
|
-
receive_200
|
399
|
+
receive_200(options.merge(opts)) do |recv|
|
400
|
+
recv << doc.create_element('action') do |action|
|
401
|
+
action << doc.create_element('ereg') do |ereg|
|
402
|
+
ereg['regexp'] = '<sip:(.*)>.*;tag=([^;]*)'
|
403
|
+
ereg['search_in'] = 'hdr'
|
404
|
+
ereg['header'] = 'To:'
|
405
|
+
ereg['assign_to'] = 'dummy,remote_addr,remote_tag'
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
# These variables will only be used if we initiate a hangup
|
410
|
+
@reference_variables += %w(dummy remote_addr remote_tag)
|
250
411
|
end
|
251
412
|
|
252
413
|
#
|
@@ -255,13 +416,16 @@ a=fmtp:101 0-15
|
|
255
416
|
# @param [Hash] opts A set of options to modify the expectation
|
256
417
|
# @option opts [true, false] :optional Whether or not receipt of the message is optional. Defaults to false.
|
257
418
|
#
|
258
|
-
def receive_ok(opts = {})
|
259
|
-
recv({ response: 200 }.merge(opts))
|
419
|
+
def receive_ok(opts = {}, &block)
|
420
|
+
recv({ response: 200 }.merge(opts), &block)
|
260
421
|
end
|
261
422
|
alias :receive_200 :receive_ok
|
262
423
|
|
263
424
|
#
|
264
|
-
#
|
425
|
+
# Convenience method to wait for an answer from the called party
|
426
|
+
#
|
427
|
+
# This sets expectations for optional SIP 100, 180 and 183,
|
428
|
+
# followed by a required 200 and sending the acknowledgement.
|
265
429
|
#
|
266
430
|
# @param [Hash] opts A set of options to modify the expectations
|
267
431
|
#
|
@@ -270,10 +434,11 @@ a=fmtp:101 0-15
|
|
270
434
|
receive_ringing opts
|
271
435
|
receive_progress opts
|
272
436
|
receive_answer opts
|
437
|
+
ack_answer opts
|
273
438
|
end
|
274
439
|
|
275
440
|
#
|
276
|
-
# Acknowledge a received answer message
|
441
|
+
# Acknowledge a received answer message and start media playback
|
277
442
|
#
|
278
443
|
# @param [Hash] opts A set of options to modify the message parameters
|
279
444
|
#
|
@@ -281,12 +446,12 @@ a=fmtp:101 0-15
|
|
281
446
|
msg = <<-BODY
|
282
447
|
|
283
448
|
ACK [next_url] SIP/2.0
|
284
|
-
Via: SIP/2.0/[transport]
|
285
|
-
From: "#{@from_user}" <sip:#{@from_user}@[
|
449
|
+
Via: SIP/2.0/[transport] #{@adv_ip}:[local_port];branch=[branch]
|
450
|
+
From: "#{@from_user}" <sip:#{@from_user}@#{@adv_ip}:[local_port]>;tag=[call_number]
|
286
451
|
To: <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
|
287
452
|
Call-ID: [call_id]
|
288
453
|
CSeq: [cseq] ACK
|
289
|
-
Contact: <sip
|
454
|
+
Contact: <sip:[$local_addr];transport=[transport]>
|
290
455
|
Max-Forwards: 100
|
291
456
|
User-Agent: #{USER_AGENT}
|
292
457
|
Content-Length: 0
|
@@ -304,7 +469,7 @@ Content-Length: 0
|
|
304
469
|
def sleep(seconds)
|
305
470
|
milliseconds = (seconds.to_f * MSEC).to_i
|
306
471
|
pause milliseconds
|
307
|
-
@media << "silence:#{milliseconds}"
|
472
|
+
@media << "silence:#{milliseconds}" if @media
|
308
473
|
end
|
309
474
|
|
310
475
|
#
|
@@ -319,6 +484,7 @@ Content-Length: 0
|
|
319
484
|
# send_digits '1234'
|
320
485
|
#
|
321
486
|
def send_digits(digits)
|
487
|
+
raise "Media not started" unless @media
|
322
488
|
delay = (0.250 * MSEC).to_i # FIXME: Need to pass this down to the media layer
|
323
489
|
digits.split('').each do |digit|
|
324
490
|
raise ArgumentError, "Invalid DTMF digit requested: #{digit}" unless VALID_DTMF.include? digit
|
@@ -331,12 +497,12 @@ Content-Length: 0
|
|
331
497
|
info = <<-INFO
|
332
498
|
|
333
499
|
INFO [next_url] SIP/2.0
|
334
|
-
Via: SIP/2.0/[transport]
|
335
|
-
From: "#{@from_user}" <sip:#{@from_user}@[
|
500
|
+
Via: SIP/2.0/[transport] #{@adv_ip}:[local_port];branch=[branch]
|
501
|
+
From: "#{@from_user}" <sip:#{@from_user}@#{@adv_ip}:[local_port]>;tag=[call_number]
|
336
502
|
To: <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
|
337
503
|
Call-ID: [call_id]
|
338
504
|
CSeq: [cseq] INFO
|
339
|
-
Contact: <sip
|
505
|
+
Contact: <sip:[$local_addr];transport=[transport]>
|
340
506
|
Max-Forwards: 100
|
341
507
|
User-Agent: #{USER_AGENT}
|
342
508
|
[routes]
|
@@ -370,18 +536,17 @@ Duration=#{delay}
|
|
370
536
|
if regexp
|
371
537
|
action = Nokogiri::XML::Node.new 'action', doc
|
372
538
|
ereg = Nokogiri::XML::Node.new 'ereg', doc
|
373
|
-
ref = Nokogiri::XML::Node.new 'Reference', doc
|
374
539
|
|
375
540
|
ereg['regexp'] = regexp
|
376
541
|
ereg['search_in'] = 'body'
|
377
542
|
ereg['check_it'] = true
|
378
543
|
|
379
544
|
var = "message_#{@message_variables += 1}"
|
380
|
-
ereg['assign_to'] =
|
545
|
+
ereg['assign_to'] = var
|
546
|
+
@reference_variables << var
|
381
547
|
|
382
548
|
action << ereg
|
383
549
|
recv << action
|
384
|
-
scenario_node << ref
|
385
550
|
end
|
386
551
|
|
387
552
|
okay
|
@@ -395,13 +560,13 @@ Duration=#{delay}
|
|
395
560
|
def send_bye(opts = {})
|
396
561
|
msg = <<-MSG
|
397
562
|
|
398
|
-
BYE [
|
399
|
-
Via: SIP/2.0/[transport]
|
400
|
-
From:
|
401
|
-
To: <sip:[
|
563
|
+
BYE sip:[$call_addr] SIP/2.0
|
564
|
+
Via: SIP/2.0/[transport] #{@adv_ip}:[local_port];branch=[branch]
|
565
|
+
From: <sip:[$local_addr]>;tag=[call_number]
|
566
|
+
To: <sip:[$remote_addr]>;tag=[$remote_tag]
|
567
|
+
Contact: <sip:[$local_addr];transport=[transport]>
|
402
568
|
Call-ID: [call_id]
|
403
569
|
CSeq: [cseq] BYE
|
404
|
-
Contact: <sip:#{@from_user}@[local_ip]:[local_port];transport=[transport]>
|
405
570
|
Max-Forwards: 100
|
406
571
|
User-Agent: #{USER_AGENT}
|
407
572
|
Content-Length: 0
|
@@ -433,7 +598,7 @@ SIP/2.0 200 OK
|
|
433
598
|
[last_To:]
|
434
599
|
[last_Call-ID:]
|
435
600
|
[last_CSeq:]
|
436
|
-
Contact: <sip
|
601
|
+
Contact: <sip:[$local_addr];transport=[transport]>
|
437
602
|
Max-Forwards: 100
|
438
603
|
User-Agent: #{USER_AGENT}
|
439
604
|
Content-Length: 0
|
@@ -453,6 +618,34 @@ Content-Length: 0
|
|
453
618
|
ack_bye(opts)
|
454
619
|
end
|
455
620
|
|
621
|
+
#
|
622
|
+
# Shortcut to send a BYE and wait for the acknowledgement
|
623
|
+
#
|
624
|
+
# @param [Hash] opts A set of options containing SIPp <recv> element attributes - will be passed to both the <send> and <recv> elements
|
625
|
+
#
|
626
|
+
def hangup(opts = {})
|
627
|
+
send_bye opts
|
628
|
+
receive_ok opts
|
629
|
+
end
|
630
|
+
|
631
|
+
# Create partition table for Call Length
|
632
|
+
#
|
633
|
+
# @param [Integer] min An value specifying the minimum time in milliseconds for the table
|
634
|
+
# @param [Integer] max An value specifying the maximum time in milliseconds for the table
|
635
|
+
# @param [Integer] interval An value specifying the interval in milliseconds for the table
|
636
|
+
def call_length_repartition(min, max, interval)
|
637
|
+
partition_table 'CallLengthRepartition', min.to_i, max.to_i, interval.to_i
|
638
|
+
end
|
639
|
+
|
640
|
+
# Create partition table for Response Time
|
641
|
+
#
|
642
|
+
# @param [Integer] min An value specifying the minimum time in milliseconds for the table
|
643
|
+
# @param [Integer] max An value specifying the maximum time in milliseconds for the table
|
644
|
+
# @param [Integer] interval An value specifying the interval in milliseconds for the table
|
645
|
+
def response_time_repartition(min, max, interval)
|
646
|
+
partition_table 'ResponseTimeRepartition', min.to_i, max.to_i, interval.to_i
|
647
|
+
end
|
648
|
+
|
456
649
|
#
|
457
650
|
# Dump the scenario to a SIPp XML string
|
458
651
|
#
|
@@ -474,6 +667,13 @@ Content-Length: 0
|
|
474
667
|
end
|
475
668
|
end
|
476
669
|
|
670
|
+
unless @reference_variables.empty?
|
671
|
+
scenario_node = docdup.xpath('scenario').first
|
672
|
+
scenario_node << docdup.create_element('Reference') do |ref|
|
673
|
+
ref[:variables] = @reference_variables.to_a.join ','
|
674
|
+
end
|
675
|
+
end
|
676
|
+
|
477
677
|
docdup.to_xml
|
478
678
|
end
|
479
679
|
|
@@ -494,7 +694,7 @@ Content-Length: 0
|
|
494
694
|
# scenario.compile! # Leaves files at test_scenario.xml and test_scenario.pcap
|
495
695
|
#
|
496
696
|
def compile!
|
497
|
-
unless @media.
|
697
|
+
unless @media.nil?
|
498
698
|
print "Compiling media to #{@filename}.pcap..."
|
499
699
|
compile_media.to_file filename: "#{@filename}.pcap"
|
500
700
|
puts "done."
|
@@ -520,8 +720,9 @@ Content-Length: 0
|
|
520
720
|
# @see http://www.ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/Tempfile.html
|
521
721
|
#
|
522
722
|
def to_tmpfiles
|
523
|
-
unless @media.empty?
|
723
|
+
unless @media.nil? || @media.empty?
|
524
724
|
media_file = Tempfile.new 'media'
|
725
|
+
media_file.binmode
|
525
726
|
media_file.write compile_media.to_s
|
526
727
|
media_file.rewind
|
527
728
|
end
|
@@ -544,6 +745,12 @@ Content-Length: 0
|
|
544
745
|
[user, domain]
|
545
746
|
end
|
546
747
|
|
748
|
+
# Split a string into space-delimited components, optionally allowing quoted groups
|
749
|
+
# Example: cars "cats and dogs" fish 'hammers' => ["cars", "cats and dogs", "fish", "hammers"]
|
750
|
+
def split_quoted_string(args)
|
751
|
+
args.to_s.scan(/'.+?'|".+?"|[^ ]+/).map { |s| s.gsub /^['"]|['"]$/, '' }
|
752
|
+
end
|
753
|
+
|
547
754
|
def doc
|
548
755
|
@doc ||= begin
|
549
756
|
Nokogiri::XML::Builder.new do |xml|
|
@@ -560,9 +767,6 @@ Content-Length: 0
|
|
560
767
|
end
|
561
768
|
|
562
769
|
def parse_args(args)
|
563
|
-
raise ArgumentError, "Must include source IP:PORT" unless args.has_key? :source
|
564
|
-
raise ArgumentError, "Must include destination IP:PORT" unless args.has_key? :destination
|
565
|
-
|
566
770
|
if args[:dtmf_mode]
|
567
771
|
@dtmf_mode = args[:dtmf_mode].to_sym
|
568
772
|
raise ArgumentError, "dtmf_mode must be rfc2833 or info" unless [:rfc2833, :info].include?(@dtmf_mode)
|
@@ -570,25 +774,26 @@ Content-Length: 0
|
|
570
774
|
@dtmf_mode = :rfc2833
|
571
775
|
end
|
572
776
|
|
573
|
-
@from_addr, @from_port = args[:source].split ':'
|
574
|
-
@to_addr, @to_port = args[:destination].split ':'
|
777
|
+
@from_addr, @from_port = args[:source].split ':' if args[:source]
|
778
|
+
@to_addr, @to_port = args[:destination].split ':' if args[:destination]
|
575
779
|
@from_user = args[:from_user] || "sipp"
|
576
780
|
end
|
577
781
|
|
578
782
|
def compile_media
|
783
|
+
raise "Media not started" unless @media
|
579
784
|
@media.compile!
|
580
785
|
end
|
581
786
|
|
582
|
-
def register_message(domain, user
|
787
|
+
def register_message(domain, user)
|
583
788
|
<<-BODY
|
584
789
|
|
585
790
|
REGISTER sip:#{domain} SIP/2.0
|
586
|
-
Via: SIP/2.0/[transport]
|
791
|
+
Via: SIP/2.0/[transport] #{@adv_ip}:[local_port];branch=[branch]
|
587
792
|
From: <sip:#{user}@#{domain}>;tag=[call_number]
|
588
793
|
To: <sip:#{user}@#{domain}>
|
589
794
|
Call-ID: [call_id]
|
590
795
|
CSeq: [cseq] REGISTER
|
591
|
-
Contact: <sip:#{@from_user}@
|
796
|
+
Contact: <sip:#{@from_user}@#{@adv_ip}:[local_port];transport=[transport]>
|
592
797
|
Max-Forwards: 10
|
593
798
|
Expires: 120
|
594
799
|
User-Agent: #{USER_AGENT}
|
@@ -596,17 +801,16 @@ Content-Length: 0
|
|
596
801
|
BODY
|
597
802
|
end
|
598
803
|
|
599
|
-
def register_auth(domain, user, password
|
600
|
-
recv response: '401', auth: true, optional: false
|
804
|
+
def register_auth(domain, user, password)
|
601
805
|
<<-AUTH
|
602
806
|
|
603
807
|
REGISTER sip:#{domain} SIP/2.0
|
604
|
-
Via: SIP/2.0/[transport]
|
808
|
+
Via: SIP/2.0/[transport] #{@adv_ip}:[local_port];branch=[branch]
|
605
809
|
From: <sip:#{user}@#{domain}>;tag=[call_number]
|
606
810
|
To: <sip:#{user}@#{domain}>
|
607
811
|
Call-ID: [call_id]
|
608
812
|
CSeq: [cseq] REGISTER
|
609
|
-
Contact: <sip:#{@from_user}@
|
813
|
+
Contact: <sip:#{@from_user}@#{@adv_ip}:[local_port];transport=[transport]>
|
610
814
|
Max-Forwards: 20
|
611
815
|
Expires: 3600
|
612
816
|
[authentication username=#{user} password=#{password}]
|
@@ -616,6 +820,7 @@ Content-Length: 0
|
|
616
820
|
end
|
617
821
|
|
618
822
|
def start_media
|
823
|
+
@media = Media.new '127.0.0.255', 55555, '127.255.255.255', 44444
|
619
824
|
nop = doc.create_element('nop') { |nop|
|
620
825
|
nop << doc.create_element('action') { |action|
|
621
826
|
action << doc.create_element('exec')
|
@@ -640,15 +845,17 @@ Content-Length: 0
|
|
640
845
|
send << "\n"
|
641
846
|
send << Nokogiri::XML::CDATA.new(doc, msg)
|
642
847
|
send << "\n" #Newlines are required before and after CDATA so SIPp will parse properly
|
848
|
+
yield send if block_given?
|
643
849
|
scenario_node << send
|
644
850
|
end
|
645
851
|
|
646
|
-
def recv(opts = {})
|
852
|
+
def recv(opts = {}, &block)
|
647
853
|
raise ArgumentError, "Receive must include either a response or a request" unless opts.keys.include?(:response) || opts.keys.include?(:request)
|
648
854
|
recv = Nokogiri::XML::Node.new 'recv', doc
|
649
855
|
opts.each do |k,v|
|
650
856
|
recv[k.to_s] = v
|
651
857
|
end
|
858
|
+
yield recv if block_given?
|
652
859
|
scenario_node << recv
|
653
860
|
end
|
654
861
|
|
@@ -660,5 +867,12 @@ Content-Length: 0
|
|
660
867
|
def handle_response(code, opts)
|
661
868
|
optional_recv opts.merge(response: code)
|
662
869
|
end
|
870
|
+
|
871
|
+
def partition_table(name, min, max, interval)
|
872
|
+
range = Range.new(min, max).step interval
|
873
|
+
partition_table = Nokogiri::XML::Node.new name, doc
|
874
|
+
partition_table[:value] = range.inject{ |n,m| "#{n},#{m}"}
|
875
|
+
scenario_node << partition_table
|
876
|
+
end
|
663
877
|
end
|
664
878
|
end
|