blitz 0.1.20 → 0.1.21
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.
- data/Gemfile +7 -2
- data/Gemfile.lock +8 -1
- data/README.md +26 -0
- data/Rakefile +5 -5
- data/blitz.gemspec +21 -10
- data/lib/blitz.rb +2 -1
- data/lib/blitz/command.rb +2 -32
- data/lib/blitz/command/curl.rb +13 -195
- data/lib/blitz/curl.rb +226 -0
- data/lib/blitz/curl/error.rb +1 -1
- data/lib/blitz/curl/rush.rb +12 -19
- data/lib/blitz/curl/sprint.rb +13 -17
- data/lib/blitz/utils.rb +38 -0
- data/spec/blitz/client_spec.rb +57 -0
- data/spec/blitz/command/api_spec.rb +42 -0
- data/spec/blitz/command/curl_spec.rb +4 -0
- data/spec/blitz/curl/rush_spec.rb +89 -0
- data/spec/blitz/curl/sprint_spec.rb +86 -0
- data/spec/blitz/curl_spec.rb +242 -0
- data/spec/spec_helper.rb +13 -0
- metadata +29 -26
- data/spec/command/curl_spec.rb +0 -244
data/lib/blitz/curl.rb
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
require 'blitz/utils'
|
2
|
+
|
3
|
+
class Blitz
|
4
|
+
class Curl
|
5
|
+
extend Blitz::Utils
|
6
|
+
|
7
|
+
RE_WS = /^\s+/.freeze
|
8
|
+
RE_NOT_WS = /^[^\s]+/.freeze
|
9
|
+
RE_DQ_STRING = /^"[^"\\\r\n]*(?:\\.[^"\\\r\n]*)*"/.freeze
|
10
|
+
RE_SQ_STRING = /^'[^'\\\r\n]*(?:\\.[^'\\\r\n]*)*'/.freeze
|
11
|
+
|
12
|
+
def self.parse arguments
|
13
|
+
argv = arguments.is_a?(Array) ? arguments : xargv(arguments)
|
14
|
+
args = parse_cli argv
|
15
|
+
raise "help" if args['help']
|
16
|
+
if not args['pattern']
|
17
|
+
Blitz::Curl::Sprint.new args
|
18
|
+
else
|
19
|
+
Blitz::Curl::Rush.new args
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def self.xargv text
|
26
|
+
argv = []
|
27
|
+
while not text.empty?
|
28
|
+
if text.match RE_WS
|
29
|
+
text = $'
|
30
|
+
elsif text.match RE_DQ_STRING or text.match RE_SQ_STRING or text.match RE_NOT_WS
|
31
|
+
text = $'
|
32
|
+
argv << strip_quotes($&)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
argv
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.strip_quotes text
|
39
|
+
text[1, (text.size - 2)]
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.parse_cli argv
|
43
|
+
hash = { 'steps' => [] }
|
44
|
+
|
45
|
+
while not argv.empty?
|
46
|
+
hash['steps'] << Hash.new
|
47
|
+
step = hash['steps'].last
|
48
|
+
|
49
|
+
while not argv.empty?
|
50
|
+
break if argv.first[0,1] != '-'
|
51
|
+
|
52
|
+
k = argv.shift
|
53
|
+
if [ '-A', '--user-agent' ].member? k
|
54
|
+
step['user-agent'] = shift(k, argv)
|
55
|
+
next
|
56
|
+
end
|
57
|
+
|
58
|
+
if [ '-b', '--cookie' ].member? k
|
59
|
+
step['cookies'] ||= []
|
60
|
+
step['cookies'] << shift(k, argv)
|
61
|
+
next
|
62
|
+
end
|
63
|
+
|
64
|
+
if [ '-d', '--data' ].member? k
|
65
|
+
step['content'] ||= Hash.new
|
66
|
+
step['content']['data'] ||= []
|
67
|
+
v = shift(k, argv)
|
68
|
+
v = File.read v[1..-1] if v =~ /^@/
|
69
|
+
step['content']['data'] << v
|
70
|
+
next
|
71
|
+
end
|
72
|
+
|
73
|
+
if [ '-D', '--dump-header' ].member? k
|
74
|
+
hash['dump-header'] = shift(k, argv)
|
75
|
+
next
|
76
|
+
end
|
77
|
+
|
78
|
+
if [ '-e', '--referer'].member? k
|
79
|
+
step['referer'] = shift(k, argv)
|
80
|
+
next
|
81
|
+
end
|
82
|
+
|
83
|
+
if [ '-h', '--help' ].member? k
|
84
|
+
hash['help'] = true
|
85
|
+
next
|
86
|
+
end
|
87
|
+
|
88
|
+
if [ '-H', '--header' ].member? k
|
89
|
+
step['headers'] ||= []
|
90
|
+
step['headers'].push shift(k, argv)
|
91
|
+
next
|
92
|
+
end
|
93
|
+
|
94
|
+
if [ '-p', '--pattern' ].member? k
|
95
|
+
v = shift(k, argv)
|
96
|
+
v.split(',').each do |vt|
|
97
|
+
unless /^(\d+)-(\d+):(\d+)$/ =~ vt
|
98
|
+
raise Test::Unit::AssertionFailedError,
|
99
|
+
"invalid ramp pattern"
|
100
|
+
end
|
101
|
+
hash['pattern'] ||= { 'iterations' => 1, 'intervals' => [] }
|
102
|
+
hash['pattern']['intervals'] << {
|
103
|
+
'iterations' => 1,
|
104
|
+
'start' => $1.to_i,
|
105
|
+
'end' => $2.to_i,
|
106
|
+
'duration' => $3.to_i
|
107
|
+
}
|
108
|
+
end
|
109
|
+
next
|
110
|
+
end
|
111
|
+
|
112
|
+
if [ '-r', '--region' ].member? k
|
113
|
+
hash['region'] = shift(k, argv)
|
114
|
+
next
|
115
|
+
end
|
116
|
+
|
117
|
+
if [ '-s', '--status' ].member? k
|
118
|
+
step['status'] = shift(k, argv).to_i
|
119
|
+
next
|
120
|
+
end
|
121
|
+
|
122
|
+
if [ '-T', '--timeout' ].member? k
|
123
|
+
step['timeout'] = shift(k, argv).to_i
|
124
|
+
next
|
125
|
+
end
|
126
|
+
|
127
|
+
if [ '-u', '--user' ].member? k
|
128
|
+
step['user'] = shift(k, argv)
|
129
|
+
next
|
130
|
+
end
|
131
|
+
|
132
|
+
if [ '-X', '--request' ].member? k
|
133
|
+
step['request'] = shift(k, argv)
|
134
|
+
next
|
135
|
+
end
|
136
|
+
|
137
|
+
if /-x:c/ =~ k or /--xtract:cookie/ =~ k
|
138
|
+
xname = shift(k, argv)
|
139
|
+
assert_match /^[a-zA-Z_][a-zA-Z_0-9]*$/, xname,
|
140
|
+
"cookie name must be alphanumeric: #{xname}"
|
141
|
+
|
142
|
+
step['xtracts'] ||= Hash.new
|
143
|
+
xhash = step['xtracts'][xname] = { 'type' => 'cookie' }
|
144
|
+
next
|
145
|
+
end
|
146
|
+
|
147
|
+
if /-v:(\S+)/ =~ k or /--variable:(\S+)/ =~ k
|
148
|
+
vname = $1
|
149
|
+
vargs = shift(k, argv)
|
150
|
+
|
151
|
+
assert_match /^[a-zA-Z][a-zA-Z0-9]*$/, vname,
|
152
|
+
"variable name must be alphanumeric: #{vname}"
|
153
|
+
|
154
|
+
step['variables'] ||= Hash.new
|
155
|
+
vhash = step['variables'][vname] = Hash.new
|
156
|
+
if vargs.match /^(list)?\[([^\]]+)\]$/
|
157
|
+
vhash['type'] = 'list'
|
158
|
+
vhash['entries'] = $2.split(',')
|
159
|
+
elsif vargs.match /^(a|alpha)$/
|
160
|
+
vhash['type'] = 'alpha'
|
161
|
+
elsif vargs.match /^(a|alpha)\[(\d+),(\d+)(,(\d+))??\]$/
|
162
|
+
vhash['type'] = 'alpha'
|
163
|
+
vhash['min'] = $2.to_i
|
164
|
+
vhash['max'] = $3.to_i
|
165
|
+
vhash['count'] = $5 ? $5.to_i : 1000
|
166
|
+
elsif vargs.match /^(n|number)$/
|
167
|
+
vhash['type'] = 'number'
|
168
|
+
elsif vargs.match /^(n|number)\[(-?\d+),(-?\d+)(,(\d+))?\]$/
|
169
|
+
vhash['type'] = 'number'
|
170
|
+
vhash['min'] = $2.to_i
|
171
|
+
vhash['max'] = $3.to_i
|
172
|
+
vhash['count'] = $5 ? $5.to_i : 1000
|
173
|
+
elsif vargs.match /^(u|udid)$/
|
174
|
+
vhash['type'] = 'udid'
|
175
|
+
else
|
176
|
+
raise ArgumentError,
|
177
|
+
"Invalid variable args for #{vname}: #{vargs}"
|
178
|
+
end
|
179
|
+
next
|
180
|
+
end
|
181
|
+
|
182
|
+
if [ '-V', '--verbose' ].member? k
|
183
|
+
hash['verbose'] = true
|
184
|
+
next
|
185
|
+
end
|
186
|
+
|
187
|
+
if [ '-1', '--tlsv1' ].member? k
|
188
|
+
step['ssl'] = 'tlsv1'
|
189
|
+
next
|
190
|
+
end
|
191
|
+
|
192
|
+
if [ '-2', '--sslv2' ].member? k
|
193
|
+
step['ssl'] = 'sslv2'
|
194
|
+
next
|
195
|
+
end
|
196
|
+
|
197
|
+
if [ '-3', '--sslv3' ].member? k
|
198
|
+
step['ssl'] = 'sslv3'
|
199
|
+
next
|
200
|
+
end
|
201
|
+
|
202
|
+
raise ArgumentError, "Unknown option #{k}"
|
203
|
+
end
|
204
|
+
|
205
|
+
if step.member? 'content'
|
206
|
+
data_size = step['content']['data'].inject(0) { |m, v| m + v.size }
|
207
|
+
assert(data_size < 10*1024, "POST content must be < 10K")
|
208
|
+
end
|
209
|
+
|
210
|
+
break if hash['help']
|
211
|
+
|
212
|
+
url = argv.shift
|
213
|
+
raise ArgumentError, "no URL specified!" if not url
|
214
|
+
step['url'] = url
|
215
|
+
end
|
216
|
+
|
217
|
+
if not hash['help']
|
218
|
+
if hash['steps'].empty?
|
219
|
+
raise ArgumentError, "no URL specified!"
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
hash
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
data/lib/blitz/curl/error.rb
CHANGED
data/lib/blitz/curl/rush.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
class Blitz
|
2
|
-
|
2
|
+
class Curl # :nodoc:
|
3
3
|
# Use this to run a rush (a load test) against your app. The return values
|
4
4
|
# include the entire timeline containing the average duration, the concurrency,
|
5
5
|
# the bytes sent/received, etc.
|
@@ -99,41 +99,34 @@ class Rush
|
|
99
99
|
# the pattern. If a block is given, it's invoked periodically with the
|
100
100
|
# partial results of the run (to report progress, perhaps)
|
101
101
|
#
|
102
|
-
#
|
103
|
-
# :url => 'http://www.mudynamics.com',
|
104
|
-
# :headers => [ 'X-API-Token: foo' ],
|
105
|
-
# :region => 'california',
|
106
|
-
# :pattern => {
|
107
|
-
# :intervals => [{ :start => 1, :end => 10000, :duration => 60 }]
|
108
|
-
# }
|
109
|
-
# }
|
110
|
-
#
|
111
|
-
# result = Blitz::Curl::Sprint.execute args do |partial|
|
102
|
+
# result = Blitz::Curl.parse('-r california -p 10-50:30 www.example.com').execute do |partial|
|
112
103
|
# pp [ partial.region, partial.timeline.last.hits ]
|
113
104
|
# end
|
114
105
|
#
|
115
106
|
# You can easily export the result to JSON, XML or compute the various
|
116
107
|
# rates, etc.
|
117
|
-
def
|
118
|
-
|
108
|
+
def execute &block # |result|
|
109
|
+
queue
|
110
|
+
result &block
|
119
111
|
end
|
120
112
|
|
121
|
-
def
|
113
|
+
def queue # :nodoc:
|
122
114
|
if not args.member? 'pattern' and not args.member? :pattern
|
123
115
|
raise ArgumentError, 'missing pattern'
|
124
116
|
end
|
125
117
|
|
126
118
|
res = Command::API.client.curl_execute args
|
127
119
|
raise Error.new(res) if res['error']
|
128
|
-
|
120
|
+
@job_id = res['job_id']
|
121
|
+
@region = res['region']
|
129
122
|
end
|
130
123
|
|
131
124
|
attr_reader :job_id # :nodoc:
|
132
125
|
attr_reader :region # :nodoc:
|
126
|
+
attr_reader :args # :nodoc:
|
133
127
|
|
134
|
-
def initialize
|
135
|
-
@
|
136
|
-
@region = json['region']
|
128
|
+
def initialize args # :nodoc:
|
129
|
+
@args = args
|
137
130
|
end
|
138
131
|
|
139
132
|
def result &block # :nodoc:
|
@@ -180,7 +173,7 @@ class Rush
|
|
180
173
|
|
181
174
|
def abort! # :nodoc:
|
182
175
|
Command::API.client.abort_job job_id rescue nil
|
183
|
-
end
|
176
|
+
end
|
184
177
|
end
|
185
178
|
end # Curl
|
186
179
|
end # Blitz
|
data/lib/blitz/curl/sprint.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
class Blitz
|
2
|
-
|
2
|
+
class Curl # :nodoc:
|
3
3
|
# Use this to run a sprint against your app. The return values include the response
|
4
4
|
# time, the region from which the sprint was run along with the full request
|
5
5
|
# and response headers and the response body.
|
@@ -104,38 +104,34 @@ class Sprint
|
|
104
104
|
# The primary method to execute a sprint from region. This method supports
|
105
105
|
# all of the arguments that the blitz bar supports. For example:
|
106
106
|
#
|
107
|
-
#
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
# }
|
112
|
-
#
|
113
|
-
# result = Blitz::Curl::Sprint.execute args
|
114
|
-
def self.execute args
|
115
|
-
self.queue(args).result
|
107
|
+
# result = Blitz::Curl.parse('-r california www.example.com').execute
|
108
|
+
def execute
|
109
|
+
queue
|
110
|
+
result
|
116
111
|
end
|
117
112
|
|
118
|
-
def
|
113
|
+
def queue # :nodoc:
|
119
114
|
args.delete 'pattern'
|
120
115
|
args.delete :pattern
|
121
116
|
|
122
117
|
res = Command::API.client.curl_execute args
|
123
118
|
raise Error.new(res) if res['error']
|
124
|
-
|
119
|
+
@job_id = res['job_id']
|
120
|
+
@region = res['region']
|
125
121
|
end
|
126
122
|
|
127
123
|
attr_reader :job_id # :nodoc:
|
128
124
|
attr_reader :region # :nodoc:
|
125
|
+
attr_reader :args # :nodoc:
|
129
126
|
|
130
|
-
def initialize
|
131
|
-
@
|
132
|
-
@region = json['region']
|
127
|
+
def initialize args # :nodoc:
|
128
|
+
@args = args
|
133
129
|
end
|
134
130
|
|
135
131
|
def result # :nodoc:
|
136
132
|
while true
|
137
133
|
sleep 2.0
|
138
|
-
|
134
|
+
|
139
135
|
job = Command::API.client.job_status job_id
|
140
136
|
if job['error']
|
141
137
|
raise Error
|
@@ -170,7 +166,7 @@ class Sprint
|
|
170
166
|
|
171
167
|
def abort # :nodoc:
|
172
168
|
Command::API.client.abort_job job_id rescue nil
|
173
|
-
end
|
169
|
+
end
|
174
170
|
end
|
175
171
|
end # Curl
|
176
172
|
end # Blitz
|
data/lib/blitz/utils.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'test/unit/assertions'
|
2
|
+
|
3
|
+
# The default template string contains what was sent and received. Strip
|
4
|
+
# these out since we don't need them
|
5
|
+
unless RUBY_VERSION =~ /^1.9/
|
6
|
+
module Test # :nodoc:
|
7
|
+
module Unit # :nodoc:
|
8
|
+
module Assertions # :nodoc:
|
9
|
+
class AssertionMessage # :nodoc:
|
10
|
+
alias :old_template :template
|
11
|
+
|
12
|
+
def template
|
13
|
+
@template_string = ''
|
14
|
+
@parameters = []
|
15
|
+
old_template
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
else
|
22
|
+
module ::Test::Unit # :nodoc:
|
23
|
+
AssertionFailedError = MiniTest::Assertion
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Blitz
|
28
|
+
module Utils
|
29
|
+
include Test::Unit::Assertions
|
30
|
+
|
31
|
+
def shift key, argv
|
32
|
+
val = argv.shift
|
33
|
+
assert_not_nil(val, "missing value for #{key}")
|
34
|
+
assert_no_match(/^-.*$/, val, "missing value for #{key}")
|
35
|
+
val
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Blitz::Client do
|
4
|
+
before :each do
|
5
|
+
@resource = mock RestClient::Resource
|
6
|
+
RestClient::Resource.stub!(:new).and_return @resource
|
7
|
+
@client = Blitz::Client.new "test@example.com", '123456'
|
8
|
+
end
|
9
|
+
|
10
|
+
after :each do
|
11
|
+
RestClient::Resource.unstub!(:new)
|
12
|
+
end
|
13
|
+
|
14
|
+
context "#login" do
|
15
|
+
before :each do
|
16
|
+
@resource.should_receive(:[]).with('/login/api').and_return @resource
|
17
|
+
@resource.should_receive(:get).and_return "{\"api_key\":\"abc123\"}"
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should return an api_key" do
|
21
|
+
result = @client.login
|
22
|
+
result.should_not be_nil
|
23
|
+
result['api_key'].should == 'abc123'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context "#account_about" do
|
28
|
+
before :each do
|
29
|
+
json = "{\"api_key\":\"abc123\", \"profile\":{\"email\":\"test@example.com\"}}"
|
30
|
+
@resource.should_receive(:[]).with('/api/1/account/about').and_return @resource
|
31
|
+
@resource.should_receive(:get).and_return json
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should return a profile" do
|
35
|
+
result = @client.account_about
|
36
|
+
result.should_not be_nil
|
37
|
+
result['profile'].should_not be_nil
|
38
|
+
result['profile']['email'].should == "test@example.com"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "#curl_execute" do
|
43
|
+
before :each do
|
44
|
+
json = "{\"ok\":true, \"job_id\":\"j123\", \"status\":\"queued\"}"
|
45
|
+
@resource.should_receive(:[]).with('/api/1/curl/execute').and_return @resource
|
46
|
+
@resource.should_receive(:post).and_return json
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should return a profile" do
|
50
|
+
result = @client.curl_execute "{\"url\":\"wwwexample.com\"}"
|
51
|
+
result.should_not be_nil
|
52
|
+
result['ok'].should be_true
|
53
|
+
result['status'].should == "queued"
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|