stella 0.7.0.006 → 0.7.0.012
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.txt +1 -0
- data/bin/stella +2 -2
- data/examples/cookies/plan.rb +18 -0
- data/examples/essentials/plan.rb +216 -29
- data/examples/example_webapp.rb +14 -4
- data/examples/example_webapp.ru +6 -0
- data/examples/exceptions/plan.rb +2 -8
- data/lib/stella/cli.rb +6 -3
- data/lib/stella/client/container.rb +49 -0
- data/lib/stella/client/modifiers.rb +19 -0
- data/lib/stella/client.rb +50 -78
- data/lib/stella/data/http/request.rb +2 -2
- data/lib/stella/engine/functional.rb +19 -24
- data/lib/stella/engine/load.rb +122 -52
- data/lib/stella/engine.rb +25 -4
- data/lib/stella/mixins/thread.rb +6 -0
- data/lib/stella/stats.rb +22 -36
- data/lib/stella/testplan/usecase.rb +2 -0
- data/lib/stella/testplan.rb +14 -5
- data/lib/stella/version.rb +1 -1
- data/lib/stella.rb +7 -0
- data/stella.gemspec +7 -4
- data/vendor/httpclient-2.1.5.2/httpclient/cookie.rb +71 -71
- data/vendor/httpclient-2.1.5.2/httpclient/http.rb +4 -1
- data/vendor/httpclient-2.1.5.2/httpclient/session.rb +3 -3
- data/vendor/httpclient-2.1.5.2/httpclient/ssl_config.rb +10 -6
- data/vendor/httpclient-2.1.5.2/httpclient.rb +3 -4
- metadata +8 -5
- data/vendor/httpclient-2.1.5.2/httpclient/stats.rb +0 -90
data/CHANGES.txt
CHANGED
data/bin/stella
CHANGED
@@ -59,7 +59,7 @@ class Stella::CLI::Definition
|
|
59
59
|
about "Run a functional test"
|
60
60
|
usage "stella verify http://stellaaahhhh.com/"
|
61
61
|
usage "stella verify -p path/2/testplan.rb http://stellaaahhhh.com/"
|
62
|
-
option :
|
62
|
+
option :w, :nowait, "Ignore wait times"
|
63
63
|
option :p, :testplan, String, "Path to testplan"
|
64
64
|
command :verify => Stella::CLI
|
65
65
|
|
@@ -68,7 +68,7 @@ class Stella::CLI::Definition
|
|
68
68
|
usage "stella load http://stellaaahhhh.com:3114/"
|
69
69
|
usage "stella load --clients=10 --repetitions=2 http://stellaaahhhh.com/"
|
70
70
|
usage "stella load -p path/2/testplan.rb -u 100 -r 5 http://stellaaahhhh.com/"
|
71
|
-
option :
|
71
|
+
option :w, :nowait, "Ignore wait times"
|
72
72
|
option :c, :clients, Integer, "Number of virtual clients"
|
73
73
|
option :r, :repetitions, Integer, "Number of times to repeat the testplan (per vclient)"
|
74
74
|
option :t, :time, String, "Max duration to run test"
|
@@ -0,0 +1,18 @@
|
|
1
|
+
desc "Maintain Your Cookies"
|
2
|
+
|
3
|
+
usecase 65, "Simple search" do
|
4
|
+
|
5
|
+
get "/", "Homepage"
|
6
|
+
|
7
|
+
get "/search", "Search Results" do
|
8
|
+
param :what => random(['Big', 'Beads'])
|
9
|
+
param :where => 'Toronto'
|
10
|
+
end
|
11
|
+
|
12
|
+
get "/", "Homepage" do
|
13
|
+
response 200 do
|
14
|
+
puts doc.css('ul#history').first
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
data/examples/essentials/plan.rb
CHANGED
@@ -1,26 +1,165 @@
|
|
1
|
-
#
|
1
|
+
# Stella - Example Test Plan
|
2
|
+
#
|
3
|
+
#
|
4
|
+
# 1. INTRODUCTION
|
5
|
+
#
|
6
|
+
# A test plan is a group of one or more user
|
7
|
+
# scenarios. This allows you to simulate the
|
8
|
+
# kind of traffic your application is exposed
|
9
|
+
# to in the wild. Realistic tests are really
|
10
|
+
# important because they give you a much more
|
11
|
+
# accurate view of how your application is
|
12
|
+
# performing.
|
13
|
+
#
|
14
|
+
# A scenario (or "usecase") is a group a
|
15
|
+
# requests that represents a typical path
|
16
|
+
# that a real person would take through
|
17
|
+
# your site.
|
18
|
+
#
|
19
|
+
# This plan contains 3 scenarios:
|
20
|
+
#
|
21
|
+
# - Simple Search (60%)
|
22
|
+
# - YAML API (30%)
|
23
|
+
# - Self-serve API (10%)
|
24
|
+
#
|
25
|
+
# The percentages represent the relative amount
|
26
|
+
# of traffic to be generated for each scenario.
|
27
|
+
# In a test with 100 virtual users, 60 would
|
28
|
+
# follow the Simple Search usecase, 30 the YAML
|
29
|
+
# API, and 10 would following the Self-Serve API.
|
30
|
+
#
|
31
|
+
#
|
32
|
+
# 2. THE CONFIGURATION LANGUAGE
|
33
|
+
#
|
34
|
+
# Test plans are defined in a streamlined version
|
35
|
+
# of the Ruby programming language. Using a "real"
|
36
|
+
# languages gives you a lot of power to specify
|
37
|
+
# complex operations.
|
38
|
+
#
|
39
|
+
#
|
40
|
+
# 3. START THE EXAMPLE APPLICATION
|
41
|
+
#
|
42
|
+
# You need to start the example web application before
|
43
|
+
# running this testplan. You can do this in one of the
|
44
|
+
# following ways:
|
45
|
+
#
|
46
|
+
# $ ruby examples/example_webapp.rb
|
47
|
+
#
|
48
|
+
# OR
|
49
|
+
#
|
50
|
+
# $ thin -R examples/example_webapp.ru start
|
51
|
+
#
|
52
|
+
# You can check that it's running by going to:
|
53
|
+
# http://127.0.0.1:3000/
|
54
|
+
#
|
55
|
+
#
|
56
|
+
# 4. RUNNING THE TEST PLAN
|
57
|
+
#
|
58
|
+
# You run this test plan from the command line.
|
59
|
+
# First you verify that the plan and application
|
60
|
+
# are running correctly:
|
61
|
+
#
|
62
|
+
# $ stella verify -p examples/essentials/plan.rb http://127.0.0.1:3114/
|
63
|
+
#
|
64
|
+
# The "verify" command executes the plan with a
|
65
|
+
# single user and provides more detailed output.
|
66
|
+
#
|
67
|
+
# "load" tests are run in a similar way:
|
68
|
+
#
|
69
|
+
# $ stella load -c 50 -r 10 -p examples/essentials/plan.rb http://127.0.0.1:3114/
|
70
|
+
#
|
71
|
+
# where "c" is the number of concurrent users and
|
72
|
+
# "r" is the number of times to repeat the plan.
|
73
|
+
#
|
74
|
+
#
|
75
|
+
# 5. WRITING A TEST PLAN
|
76
|
+
#
|
77
|
+
# The following is an example of a working test plan.
|
78
|
+
# I put a lot of effort into keeping the syntax simple
|
79
|
+
# but feel free to contact me if you have any questions
|
80
|
+
# or problems.
|
81
|
+
#
|
82
|
+
# Happy Testing!
|
83
|
+
#
|
84
|
+
# Delano (@solutious.com)
|
85
|
+
#
|
2
86
|
|
3
87
|
desc "Business Finder Testplan"
|
4
88
|
|
5
89
|
usecase 65, "Simple search" do
|
90
|
+
|
91
|
+
# An important factor in simulating traffic
|
92
|
+
# is using real, dynamic data. You can load
|
93
|
+
# data from a file using the resource method.
|
94
|
+
# Here is an example which reads a list of
|
95
|
+
# search terms ("restaurant", "hotel", ...)
|
96
|
+
# into an array called :search_terms. The
|
97
|
+
# colon is Ruby's way of defining a symbol.
|
98
|
+
#
|
6
99
|
resource :search_terms, list('search_terms.csv')
|
7
100
|
|
101
|
+
# Requests are defined with one of the
|
102
|
+
# following methods: get, post, head, delete.
|
103
|
+
# Here we define a simple get request for the
|
104
|
+
# homepage ("/").
|
105
|
+
#
|
8
106
|
get "/", "Homepage" do
|
107
|
+
# This tells Stella to wait between 1 and 5
|
108
|
+
# seconds before moving to the next request.
|
9
109
|
wait 1..5
|
10
|
-
response 200 do
|
11
|
-
status # => 200
|
12
|
-
headers['Content-Type'] # => ['text/html']
|
13
|
-
body # => <html>...
|
14
|
-
doc # => Nokigiri::HTML::Document
|
15
|
-
end
|
16
110
|
end
|
17
111
|
|
112
|
+
# In this request, the user has entered a simple
|
113
|
+
# what and where search. You'll notice that we
|
114
|
+
# aren't specifying a hostname or port number for
|
115
|
+
# these requests. We do that so you use the same
|
116
|
+
# test plan to run tests on machines with different
|
117
|
+
# hostnames. However, this is optional. You can
|
118
|
+
# also specify absolute URIs like this:
|
119
|
+
#
|
120
|
+
# get "http://example.com:8000/search"
|
121
|
+
#
|
18
122
|
get "/search", "Search Results" do
|
19
123
|
wait 2..5
|
124
|
+
|
125
|
+
# Two URI parameters will be included with this
|
126
|
+
# request. Notice that the values for the what
|
127
|
+
# and where parameters come from the resources
|
128
|
+
# we defined. We've specified for random values
|
129
|
+
# to be used, but we could also specify sequential
|
130
|
+
# or reverse sequential values (see XML API).
|
131
|
+
#
|
20
132
|
param :what => random(:search_terms)
|
21
|
-
param :where => random(['Toronto', 'Montreal'])
|
133
|
+
param :where => random(['Toronto', 'Vancouver', 'Montreal'])
|
134
|
+
|
135
|
+
# Each request can also include one or more
|
136
|
+
# optional response blocks. These blocks determine
|
137
|
+
# what should happen when the specified HTTP
|
138
|
+
# status code is returned by the web server.
|
139
|
+
#
|
140
|
+
# For successful responses, we want to parse out
|
141
|
+
# some data from the page. For 404 (Not Found)
|
142
|
+
# responses, we simply want the virtual user to
|
143
|
+
# quit the usecase altogether.
|
144
|
+
#
|
22
145
|
response 200 do
|
146
|
+
|
147
|
+
# If the response contains HTML, it will
|
148
|
+
# automatically be parsed using the Ruby
|
149
|
+
# library Nokogiri. See the following link
|
150
|
+
# for more information:
|
151
|
+
# http://nokogiri.rubyforge.org/nokogiri/
|
152
|
+
#
|
153
|
+
# The important thing to note is that you
|
154
|
+
# don't need to write complex regular
|
155
|
+
# expressions to grab data from the page.
|
156
|
+
#
|
23
157
|
listing = doc.css('div.listing').first
|
158
|
+
|
159
|
+
# Here we grab the first listing ID on the
|
160
|
+
# page and store it in a variable called
|
161
|
+
# :lid. This is similar to a resource.
|
162
|
+
#
|
24
163
|
set :lid, listing['id'].match(/(\d+)/)[0]
|
25
164
|
end
|
26
165
|
response 404 do
|
@@ -28,37 +167,66 @@ usecase 65, "Simple search" do
|
|
28
167
|
end
|
29
168
|
end
|
30
169
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
170
|
+
# Notice the special variable in this URI path.
|
171
|
+
# Since we have defined a variable with the same
|
172
|
+
# name in the previous request, the variable in
|
173
|
+
# the request will automatically be replaced with
|
174
|
+
# that value. This value is unique for each virtual
|
175
|
+
# user.
|
176
|
+
#
|
177
|
+
get "/listing/:lid" do
|
178
|
+
desc "Selected listing"
|
179
|
+
wait 1..8
|
180
|
+
end
|
35
181
|
|
36
182
|
end
|
37
183
|
|
38
|
-
|
39
|
-
post "/listing/add", "Add a listing" do
|
40
|
-
wait 1..4
|
41
|
-
param :name => random(8)
|
42
|
-
param :city => sequential("Montreal", "Toronto", "Vancouver")
|
43
|
-
param :logo => file('logo.png')
|
44
|
-
response 302 do
|
45
|
-
repeat 3
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
usecase "Listing API" do
|
184
|
+
usecase 25, "YAML API" do
|
51
185
|
|
52
186
|
get '/listings.yaml', "View All" do
|
53
187
|
response 200 do
|
54
|
-
|
188
|
+
|
189
|
+
# We showed above how HTML is parsed automatically.
|
190
|
+
# Stella can do the same with XML, YAML, and JSON.
|
191
|
+
#
|
192
|
+
# A variable called "doc" contains the parsed YAML.
|
193
|
+
# "collect" is a Ruby method that iterates through
|
194
|
+
# all items in an Array and returns a new Array
|
195
|
+
# containing the return values from each iteration.
|
196
|
+
# Each item item is available in the variable
|
197
|
+
# called 'l'.
|
198
|
+
#
|
199
|
+
# The variable called "listings" will contain all
|
200
|
+
# listing ids in the YAML document.
|
201
|
+
#
|
55
202
|
listings = doc.collect! { |l|; l[:id]; }
|
56
|
-
|
203
|
+
|
204
|
+
# And here we store that list of ids.
|
205
|
+
#
|
206
|
+
set :listing_ids, listings
|
57
207
|
end
|
58
208
|
end
|
59
209
|
|
60
|
-
|
61
|
-
|
210
|
+
# In the Simple Search usecase we stored a listing
|
211
|
+
# id in a variable called :lid. This time we have
|
212
|
+
# an Array of listing ids but we only want to use
|
213
|
+
# one for each request so we override the automatic
|
214
|
+
# variable replacement by using the param method.
|
215
|
+
#
|
216
|
+
get "/listing/:lid.yaml", "Select Listing" do
|
217
|
+
|
218
|
+
# Each request will use a new value from the
|
219
|
+
# Array and these will be selected sequentially.
|
220
|
+
# Note that this is _per virtual user_. Each
|
221
|
+
# vuser will have its own copy of the Array and
|
222
|
+
# iterate through it independently.
|
223
|
+
#
|
224
|
+
param :lid => rsequential(:listing_ids)
|
225
|
+
|
226
|
+
# We can use response blocks to affect behaviour
|
227
|
+
# the user. Here we specify that every virtual
|
228
|
+
# user should repeat this request 7 times.
|
229
|
+
#
|
62
230
|
response 200 do
|
63
231
|
repeat 7
|
64
232
|
end
|
@@ -66,3 +234,22 @@ usecase "Listing API" do
|
|
66
234
|
|
67
235
|
end
|
68
236
|
|
237
|
+
usecase 10, "Self-serve API" do
|
238
|
+
|
239
|
+
# Here is an example of using a POST request.
|
240
|
+
# Notice that the definition is otherwise
|
241
|
+
# identical to the ones you've seen above.
|
242
|
+
#
|
243
|
+
post "/listing/add", "Add a listing" do
|
244
|
+
wait 1..4
|
245
|
+
param :name => random(8)
|
246
|
+
param :city => random(['Toronto', 'Vancouver', 'Montreal'])
|
247
|
+
param :logo => file('logo.png')
|
248
|
+
response 302 do
|
249
|
+
repeat 3
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
end
|
254
|
+
|
255
|
+
# a5689bb64829d2dc1e9ab8901223cc90c975fe3a
|
data/examples/example_webapp.rb
CHANGED
@@ -73,7 +73,11 @@ post '/listing/add' do
|
|
73
73
|
else
|
74
74
|
@listings = options.listings
|
75
75
|
if find_name(params[:name], @listings).empty?
|
76
|
-
|
76
|
+
# Limit the number of in memory listings, but we want to keep
|
77
|
+
# the original listings so we use a slice instead of a shift.
|
78
|
+
if @listings.size >= options.max_listings
|
79
|
+
@listings.slice!(LISTINGS.size)
|
80
|
+
end
|
77
81
|
@listings << { :name => params[:name], :id => rand(100000), :city => params[:city] }
|
78
82
|
if params[:logo].is_a?(Hash) && params[:logo][:tempfile]
|
79
83
|
p "TODO: Fix uploads"
|
@@ -130,7 +134,8 @@ before do
|
|
130
134
|
@cookie[:history].unshift params[:what] unless blank?(params[:what])
|
131
135
|
@cookie[:history].pop if @cookie[:history].size > 5
|
132
136
|
@cookie[:location] = params[:where] unless blank?(params[:where])
|
133
|
-
response.set_cookie "bff-history",
|
137
|
+
response.set_cookie "bff-history",
|
138
|
+
:value => @cookie.to_yaml#, :expires => Time.parse('16/02/2012')
|
134
139
|
end
|
135
140
|
|
136
141
|
helpers do
|
@@ -177,11 +182,16 @@ __END__
|
|
177
182
|
@@layout
|
178
183
|
<html>
|
179
184
|
<head>
|
185
|
+
<!--
|
186
|
+
Param: __stella: <%= params['__stella'] %>
|
187
|
+
Header: X-Stella-ID: <%= env['HTTP_X_STELLA_ID'] %>
|
188
|
+
-->
|
180
189
|
<title><%= @title %></title>
|
181
190
|
<style>
|
182
191
|
.hilite { background-color: #FEE00B; font-weight: bold; }
|
183
192
|
.footer { color: #ccc; font-weight: lighter; font-size: 80%; margin-top: 30px; }
|
184
|
-
.footer a { color: #69c;}
|
193
|
+
.footer a { color: #69c; }
|
194
|
+
body { background: url('http://solutious.com/images/solutious-logo-large.png?1') no-repeat right;}
|
185
195
|
</style>
|
186
196
|
</head>
|
187
197
|
<body>
|
@@ -194,7 +204,7 @@ __END__
|
|
194
204
|
</em></p>
|
195
205
|
<%= yield %>
|
196
206
|
<div class="footer">
|
197
|
-
A <a href="http://solutious.com/
|
207
|
+
A <a href="http://solutious.com/">Solutious Inc</a> production.
|
198
208
|
</div>
|
199
209
|
</body>
|
200
210
|
</html>
|
data/examples/example_webapp.ru
CHANGED
data/examples/exceptions/plan.rb
CHANGED
@@ -3,16 +3,10 @@
|
|
3
3
|
usecase "Exception Handling" do
|
4
4
|
|
5
5
|
get "/search", "Search Results" do
|
6
|
-
|
7
|
-
param :what => random()
|
6
|
+
param :what => 'No Such Listing'
|
8
7
|
param :where => random('Toronto', 'Montreal', 'Vancouver')
|
9
|
-
response 200 do
|
10
|
-
listing = doc.css('div.listing').first
|
11
|
-
set :lid, listing['id'].match(/(\d+)/)[0]
|
12
|
-
raise NoListingResultFound if listing.nil?
|
13
|
-
end
|
14
8
|
response 404 do
|
15
|
-
raise NoSearchResults
|
9
|
+
raise NoSearchResults
|
16
10
|
end
|
17
11
|
end
|
18
12
|
|
data/lib/stella/cli.rb
CHANGED
@@ -15,7 +15,7 @@ class Stella::CLI < Drydock::Command
|
|
15
15
|
def verify
|
16
16
|
opts = {}
|
17
17
|
opts[:hosts] = @hosts
|
18
|
-
opts[:
|
18
|
+
opts[:nowait] = true if @option.nowait
|
19
19
|
ret = Stella::Engine::Functional.run @testplan, opts
|
20
20
|
@exit_code = (ret ? 0 : 1)
|
21
21
|
end
|
@@ -27,7 +27,7 @@ class Stella::CLI < Drydock::Command
|
|
27
27
|
def load
|
28
28
|
opts = {}
|
29
29
|
opts[:hosts] = @hosts
|
30
|
-
[:
|
30
|
+
[:nowait, :clients, :repetitions, :delay, :time].each do |opt|
|
31
31
|
opts[opt] = @option.send(opt) unless @option.send(opt).nil?
|
32
32
|
end
|
33
33
|
ret = Stella::Engine::Load.run @testplan, opts
|
@@ -39,7 +39,10 @@ class Stella::CLI < Drydock::Command
|
|
39
39
|
unless @option.testplan.nil? || File.exists?(@option.testplan)
|
40
40
|
raise Stella::InvalidOption, "Bad path: #{@option.testplan}"
|
41
41
|
end
|
42
|
-
@hosts = @argv.collect { |uri|;
|
42
|
+
@hosts = @argv.collect { |uri|;
|
43
|
+
uri = 'http://' << uri unless uri.match /^http:\/\//i
|
44
|
+
URI.parse uri;
|
45
|
+
}
|
43
46
|
if @option.testplan
|
44
47
|
@testplan = Stella::Testplan.load_file @option.testplan
|
45
48
|
else
|
@@ -0,0 +1,49 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class Stella::Client
|
4
|
+
|
5
|
+
class Container
|
6
|
+
|
7
|
+
attr_accessor :usecase
|
8
|
+
attr_accessor :response
|
9
|
+
attr_reader :resources
|
10
|
+
def initialize(usecase)
|
11
|
+
@usecase, @resources = usecase, {}
|
12
|
+
@base_path = usecase.base_path
|
13
|
+
end
|
14
|
+
|
15
|
+
# This is used to handle custom exception in usecases.
|
16
|
+
# See examples/exceptions/plan.rb
|
17
|
+
#
|
18
|
+
def self.const_missing(custom_error)
|
19
|
+
ResponseError.new custom_error
|
20
|
+
end
|
21
|
+
|
22
|
+
def doc
|
23
|
+
# NOTE: It's important to parse the document on every
|
24
|
+
# request because this container is available for the
|
25
|
+
# entire life of a usecase.
|
26
|
+
case @response.header['Content-Type']
|
27
|
+
when ['text/html']
|
28
|
+
Nokogiri::HTML(body)
|
29
|
+
when ['text/yaml']
|
30
|
+
YAML.load(body)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def resource(n)
|
35
|
+
return @usecase.resource(n) if @usecase.resources.has_key? n
|
36
|
+
return @resources[n] if @resources.has_key? n
|
37
|
+
end
|
38
|
+
|
39
|
+
def body; @response.body.content; end
|
40
|
+
def headers; @response.header; end
|
41
|
+
alias_method :header, :headers
|
42
|
+
def status; @response.status; end
|
43
|
+
def set(n, v); @resources[n] = v; end
|
44
|
+
def wait(t); sleep t; end
|
45
|
+
def quit(msg=nil); Quit.new(msg); end
|
46
|
+
def repeat(t=1); Repeat.new(t); end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class Stella::Client
|
4
|
+
|
5
|
+
class ResponseModifier; end
|
6
|
+
class Repeat < ResponseModifier;
|
7
|
+
attr_accessor :times
|
8
|
+
def initialize(times)
|
9
|
+
@times = times
|
10
|
+
end
|
11
|
+
end
|
12
|
+
class Quit < ResponseModifier;
|
13
|
+
attr_accessor :message
|
14
|
+
def initialize(msg=nil)
|
15
|
+
@message = msg
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|