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