stella 0.6.0 → 0.7.0.002
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/CHANGES.txt +7 -15
- data/LICENSE.txt +1 -1
- data/README.rdoc +93 -63
- data/Rakefile +32 -42
- data/bin/stella +138 -0
- data/examples/basic/listing_ids.csv +7 -0
- data/examples/basic/plan.rb +71 -0
- data/lib/stella/cli.rb +66 -0
- data/lib/stella/client.rb +199 -0
- data/lib/stella/config.rb +87 -0
- data/lib/stella/data/http/body.rb +15 -0
- data/lib/stella/data/http/request.rb +116 -0
- data/lib/stella/data/http/response.rb +92 -0
- data/lib/stella/data/http.rb +2 -257
- data/lib/stella/data.rb +85 -0
- data/lib/stella/dsl.rb +5 -0
- data/lib/stella/engine/functional.rb +39 -0
- data/lib/stella/engine/load.rb +106 -0
- data/lib/stella/engine.rb +55 -0
- data/lib/stella/exceptions.rb +15 -0
- data/lib/stella/guidelines.rb +18 -0
- data/lib/stella/mixins.rb +2 -0
- data/lib/stella/stats.rb +3 -7
- data/lib/stella/testplan/stats.rb +26 -0
- data/lib/stella/testplan/usecase.rb +67 -0
- data/lib/stella/testplan.rb +95 -220
- data/lib/{util → stella/utils}/httputil.rb +0 -0
- data/lib/stella/utils.rb +126 -0
- data/lib/stella/version.rb +15 -0
- data/lib/stella.rb +58 -104
- data/lib/threadify.rb +0 -6
- data/stella.gemspec +43 -49
- data/support/example_webapp.rb +246 -0
- data/support/useragents.txt +75 -0
- metadata +68 -32
- data/bin/example_test.rb +0 -82
- data/bin/example_webapp.rb +0 -63
- data/lib/logger.rb +0 -79
- data/lib/stella/clients.rb +0 -161
- data/lib/stella/command/base.rb +0 -20
- data/lib/stella/command/form.rb +0 -36
- data/lib/stella/command/get.rb +0 -44
- data/lib/stella/common.rb +0 -53
- data/lib/stella/crypto.rb +0 -88
- data/lib/stella/data/domain.rb +0 -82
- data/lib/stella/environment.rb +0 -66
- data/lib/stella/functest.rb +0 -105
- data/lib/stella/loadtest.rb +0 -186
- data/lib/stella/testrunner.rb +0 -64
- data/lib/storable.rb +0 -280
- data/lib/timeunits.rb +0 -65
- data/tryouts/drb/drb_test.rb +0 -65
- data/tryouts/drb/open4.rb +0 -19
- data/tryouts/drb/slave.rb +0 -27
- data/tryouts/oo_tryout.rb +0 -30
@@ -0,0 +1,199 @@
|
|
1
|
+
require "observer"
|
2
|
+
require "tempfile"
|
3
|
+
require 'httpclient'
|
4
|
+
require 'nokogiri'
|
5
|
+
|
6
|
+
module Stella
|
7
|
+
class Client
|
8
|
+
include Observable
|
9
|
+
attr_reader :client_id
|
10
|
+
attr_accessor :base_uri
|
11
|
+
attr_accessor :proxy
|
12
|
+
attr_reader :stats
|
13
|
+
def initialize(base_uri=nil, client_id=1)
|
14
|
+
@base_uri, @client_id = base_uri, client_id
|
15
|
+
@cookie_file = Tempfile.new('stella-cookie')
|
16
|
+
@stats = Stella::Stats.new("Client #{@client_id}")
|
17
|
+
end
|
18
|
+
|
19
|
+
def execute(usecase)
|
20
|
+
http_client = generate_http_client
|
21
|
+
container = Container.new(usecase)
|
22
|
+
counter = 0
|
23
|
+
usecase.requests.each do |req|
|
24
|
+
counter += 1
|
25
|
+
uri_obj = URI.parse(req.uri)
|
26
|
+
params = prepare_params(usecase, req.params)
|
27
|
+
uri = build_request_uri uri_obj, params, container
|
28
|
+
raise NoHostDefined, uri_obj if uri.host.nil? || uri.host.empty?
|
29
|
+
|
30
|
+
meth = req.http_method.to_s.downcase
|
31
|
+
Stella.ld "#{meth}: " << "#{uri_obj.to_s} " << req.params.inspect
|
32
|
+
|
33
|
+
changed and notify_observers(:send_request, @client_id, usecase, meth, uri, req, params, counter)
|
34
|
+
begin
|
35
|
+
container.response = http_client.send(meth, uri, params) # booya!
|
36
|
+
changed and notify_observers(:receive_response, @client_id, usecase, meth, uri, req, params, container)
|
37
|
+
rescue => ex
|
38
|
+
changed and notify_observers(:request_error, @client_id, usecase, meth, uri, req, params, ex)
|
39
|
+
next
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
ret = execute_response_handler container, req
|
44
|
+
|
45
|
+
Drydock::Screen.flush
|
46
|
+
|
47
|
+
if ret.kind_of?(ResponseModifier)
|
48
|
+
case ret.class.to_s
|
49
|
+
when "Stella::Client::Repeat"
|
50
|
+
Stella.ld "REPETITION: #{counter} of #{ret.times+1}"
|
51
|
+
redo if counter <= ret.times
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
counter = 0 # reset
|
56
|
+
run_sleeper(req.wait) if req.wait && !benchmark?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def enable_benchmark_mode; @bm = true; end
|
61
|
+
def disable_benchmark_mode; @bm = false; end
|
62
|
+
def benchmark?; @bm == true; end
|
63
|
+
|
64
|
+
private
|
65
|
+
def run_sleeper(wait)
|
66
|
+
if wait.is_a?(Range)
|
67
|
+
ms = rand(wait.last * 1000).to_f
|
68
|
+
ms = wait.first if ms < wait.first
|
69
|
+
else
|
70
|
+
ms = wait * 1000
|
71
|
+
end
|
72
|
+
sleep ms / 1000
|
73
|
+
end
|
74
|
+
|
75
|
+
def generate_http_client
|
76
|
+
if @proxy
|
77
|
+
http_client = HTTPClient.new(@proxy.uri)
|
78
|
+
http_client.set_proxy_auth(@proxy.user, @proxy.pass) if @proxy.user
|
79
|
+
else
|
80
|
+
http_client = HTTPClient.new
|
81
|
+
end
|
82
|
+
http_client.set_cookie_store @cookie_file.to_s
|
83
|
+
http_client
|
84
|
+
end
|
85
|
+
|
86
|
+
def prepare_params(usecase, params)
|
87
|
+
newparams = {}
|
88
|
+
params.each_pair do |n,v|
|
89
|
+
v = usecase.instance_eval &v if v.is_a?(Proc)
|
90
|
+
newparams[n] = v
|
91
|
+
end
|
92
|
+
newparams
|
93
|
+
end
|
94
|
+
|
95
|
+
# Testplan URIs can be relative or absolute. Either one can
|
96
|
+
# contain variables in the form <tt>:varname</tt>, as in:
|
97
|
+
#
|
98
|
+
# http://example.com/product/:productid
|
99
|
+
#
|
100
|
+
# This method creates a new URI object using the @base_uri
|
101
|
+
# if necessary and replaces all variables with literal values.
|
102
|
+
# If no replacement value can be found, the variable is not touched.
|
103
|
+
def build_request_uri(requri, params, container)
|
104
|
+
uri = ""
|
105
|
+
request_uri = requri.to_s
|
106
|
+
if requri.host.nil?
|
107
|
+
uri = base_uri.to_s
|
108
|
+
uri.gsub! /\/$/, '' # Don't double up on the first slash
|
109
|
+
request_uri = '/' << request_uri unless request_uri.match(/^\//)
|
110
|
+
end
|
111
|
+
# We call req.uri again because we need
|
112
|
+
# to modify request_uri inside the loop.
|
113
|
+
requri.to_s.scan(/:([a-z_]+)/i) do |instances|
|
114
|
+
instances.each do |varname|
|
115
|
+
val = find_replacement_value(varname, params, container)
|
116
|
+
#Stella.ld "FOUND: #{val}"
|
117
|
+
request_uri.gsub! /:#{varname}/, val.to_s unless val.nil?
|
118
|
+
end
|
119
|
+
end
|
120
|
+
uri << request_uri
|
121
|
+
URI.parse uri
|
122
|
+
end
|
123
|
+
|
124
|
+
# Testplan URIs can contain variables in the form <tt>:varname</tt>.
|
125
|
+
# This method looks at the request parameters and then at the
|
126
|
+
# usecase's resource hash for a replacement value.
|
127
|
+
# If not found, returns nil.
|
128
|
+
def find_replacement_value(name, params, container)
|
129
|
+
value = nil
|
130
|
+
#Stella.ld "REPLACE: #{name}"
|
131
|
+
#Stella.ld "PARAMS: #{params.inspect}"
|
132
|
+
#Stella.ld "IVARS: #{container.instance_variables}"
|
133
|
+
value = params[name.to_sym]
|
134
|
+
value = container.resource name.to_sym if value.nil?
|
135
|
+
value
|
136
|
+
end
|
137
|
+
|
138
|
+
# Find the appropriate response handler by executing the
|
139
|
+
# HTTP response status against the configured handlers.
|
140
|
+
# If several match, the first one is used.
|
141
|
+
def execute_response_handler(container, req)
|
142
|
+
handlers = req.response.select do |regex,handler|
|
143
|
+
regex = /#{regex}/ unless regex.is_a? Regexp
|
144
|
+
Stella.ld "HANDLER REGEX: #{regex} (#{container.status})"
|
145
|
+
container.status.to_s =~ regex
|
146
|
+
end
|
147
|
+
ret = nil
|
148
|
+
unless handlers.empty?
|
149
|
+
begin
|
150
|
+
changed
|
151
|
+
ret = container.instance_eval &handlers.values.first
|
152
|
+
notify_observers(:execute_response_handler, @client_id, req, container)
|
153
|
+
rescue => ex
|
154
|
+
notify_observers(:error_execute_response_handler, @client_id, ex, req, container)
|
155
|
+
Stella.ld ex.message, ex.backtrace
|
156
|
+
end
|
157
|
+
end
|
158
|
+
ret
|
159
|
+
end
|
160
|
+
|
161
|
+
class Container
|
162
|
+
attr_accessor :usecase
|
163
|
+
attr_accessor :response
|
164
|
+
def initialize(usecase)
|
165
|
+
@usecase = usecase
|
166
|
+
end
|
167
|
+
|
168
|
+
def doc
|
169
|
+
# NOTE: It's important to parse the document on every
|
170
|
+
# request because this container is available for the
|
171
|
+
# entire life of a usecase.
|
172
|
+
case @response.header['Content-Type']
|
173
|
+
when ['text/html']
|
174
|
+
Nokogiri::HTML(body)
|
175
|
+
when ['text/yaml']
|
176
|
+
YAML.load(body)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def body; @response.body.content; end
|
181
|
+
def headers; @response.header; end
|
182
|
+
alias_method :header, :headers
|
183
|
+
def status; @response.status; end
|
184
|
+
def set(n, v); usecase.resource n, v; end
|
185
|
+
def resource(n); usecase.resource n; end
|
186
|
+
def wait(t); sleep t; end
|
187
|
+
|
188
|
+
def repeat(t=1); Repeat.new(t); end
|
189
|
+
end
|
190
|
+
|
191
|
+
class ResponseModifier; end
|
192
|
+
class Repeat < ResponseModifier;
|
193
|
+
attr_accessor :times
|
194
|
+
def initialize(times)
|
195
|
+
@times = times
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class Stella::Config < Storable
|
4
|
+
include Gibbler::Complex
|
5
|
+
|
6
|
+
field :source
|
7
|
+
field :apikey
|
8
|
+
field :secret
|
9
|
+
|
10
|
+
# Returns true when the current config matches the default config
|
11
|
+
def default?; to_hash.gibbler == DEFAULT_CONFIG_HASH; end
|
12
|
+
|
13
|
+
def self.each_path(&blk)
|
14
|
+
[PROJECT_PATH, USER_PATH].each do |path|
|
15
|
+
Stella.ld "Loading #{path}"
|
16
|
+
blk.call(path) if File.exists? path
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.refresh
|
21
|
+
conf = {}
|
22
|
+
Stella::Config.each_path do |path|
|
23
|
+
tmp = YAML.load_file path
|
24
|
+
conf.merge! tmp if tmp
|
25
|
+
end
|
26
|
+
from_hash conf
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.init
|
30
|
+
raise AlreadyInitialized, PROJECT_PATH if File.exists? PROJECT_PATH
|
31
|
+
dir = File.dirname USER_PATH
|
32
|
+
Dir.mkdir(dir, 0700) unless File.exists? dir
|
33
|
+
unless File.exists? USER_PATH
|
34
|
+
Stella.li "Creating #{USER_PATH} (Add your credentials here)"
|
35
|
+
Stella::Utils.write_to_file(USER_PATH, DEFAULT_CONFIG, 'w', 0600)
|
36
|
+
end
|
37
|
+
|
38
|
+
dir = File.dirname PROJECT_PATH
|
39
|
+
Dir.mkdir(dir, 0700) unless File.exists? dir
|
40
|
+
|
41
|
+
Stella.li "Creating #{PROJECT_PATH}"
|
42
|
+
Stella::Utils.write_to_file(PROJECT_PATH, 'target:', 'w', 0600)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.blast
|
46
|
+
if File.exists? USER_PATH
|
47
|
+
Stella.li "Blasting #{USER_PATH}"
|
48
|
+
FileUtils.rm_rf File.dirname(USER_PATH)
|
49
|
+
end
|
50
|
+
if File.exists? PROJECT_PATH
|
51
|
+
Stella.li "Blasting #{PROJECT_PATH}"
|
52
|
+
FileUtils.rm_rf File.dirname(PROJECT_PATH)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def self.find_project_config
|
60
|
+
dir = Dir.pwd.split File::SEPARATOR
|
61
|
+
path = nil
|
62
|
+
while !dir.empty?
|
63
|
+
tmp = File.join(dir.join(File::SEPARATOR), DIR_NAME, 'config')
|
64
|
+
Stella.ld " -> looking for #{tmp}"
|
65
|
+
path = tmp and break if File.exists? tmp
|
66
|
+
dir.pop
|
67
|
+
end
|
68
|
+
path ||= File.join(Dir.pwd, DIR_NAME, 'config')
|
69
|
+
path
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
unless defined?(DIR_NAME)
|
74
|
+
DIR_NAME = Stella.sysinfo.os == :windows ? 'Stella' : '.stella'
|
75
|
+
USER_PATH = File.join(Stella.sysinfo.home, DIR_NAME, 'config')
|
76
|
+
PROJECT_PATH = Stella::Config.find_project_config
|
77
|
+
DEFAULT_CONFIG = <<CONF
|
78
|
+
apikey: ''
|
79
|
+
secret: ''
|
80
|
+
remote: stella.solutious.com:443
|
81
|
+
CONF
|
82
|
+
DEFAULT_CONFIG_HASH = YAML.load(DEFAULT_CONFIG).gibbler
|
83
|
+
end
|
84
|
+
|
85
|
+
class AlreadyInitialized < Stella::Error; end
|
86
|
+
end
|
87
|
+
|
@@ -0,0 +1,116 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Stella::Data::HTTP
|
4
|
+
class Request < Storable
|
5
|
+
include Gibbler::Complex
|
6
|
+
include Stella::Data::Helpers
|
7
|
+
|
8
|
+
# A hash containing blocks to be executed depending on the HTTP response status.
|
9
|
+
# The hash keys are numeric HTTP Status Codes.
|
10
|
+
#
|
11
|
+
# 200 => { ... }
|
12
|
+
# 304 => { ... }
|
13
|
+
# 500 => { ... }
|
14
|
+
#
|
15
|
+
attr_accessor :response_handler
|
16
|
+
|
17
|
+
field :desc
|
18
|
+
field :header
|
19
|
+
field :uri
|
20
|
+
field :wait
|
21
|
+
field :params
|
22
|
+
field :body
|
23
|
+
field :http_method
|
24
|
+
field :http_version
|
25
|
+
field :content_type
|
26
|
+
|
27
|
+
def has_body?
|
28
|
+
!@body.nil? && !@body.empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize (method, uri_str, version="1.1", &definition)
|
32
|
+
@uri = uri_str
|
33
|
+
@http_method, @http_version = method, version
|
34
|
+
@headers, @params, @response_handler = {}, {}, {}
|
35
|
+
@wait = 0
|
36
|
+
@desc = "Request"
|
37
|
+
@body = Stella::Data::HTTP::Body.new
|
38
|
+
instance_eval &definition unless definition.nil?
|
39
|
+
end
|
40
|
+
|
41
|
+
def desc(*args)
|
42
|
+
@desc = args.first unless args.empty?
|
43
|
+
@desc
|
44
|
+
end
|
45
|
+
|
46
|
+
def content_type(*args)
|
47
|
+
@content_type = args.first unless args.empty?
|
48
|
+
@content_type
|
49
|
+
end
|
50
|
+
|
51
|
+
def wait(*args)
|
52
|
+
@wait = args.first unless args.empty?
|
53
|
+
@wait
|
54
|
+
end
|
55
|
+
alias_method :sleep, :wait
|
56
|
+
|
57
|
+
def headers(*args)
|
58
|
+
@headers.merge! args.first unless args.empty?
|
59
|
+
@headers
|
60
|
+
end
|
61
|
+
alias_method :header, :headers
|
62
|
+
|
63
|
+
def params(*args)
|
64
|
+
@params.merge! args.first unless args.empty?
|
65
|
+
@params
|
66
|
+
end
|
67
|
+
alias_method :param, :params
|
68
|
+
|
69
|
+
def response(*args, &definition)
|
70
|
+
if definition.nil?
|
71
|
+
@response_handler
|
72
|
+
else
|
73
|
+
args << 200 if args.empty?
|
74
|
+
args.each do |status|
|
75
|
+
@response_handler[status] = definition
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# +content+ can be literal content or a file path
|
81
|
+
def body(*args)
|
82
|
+
return @body if args.empty?
|
83
|
+
content, form_param, content_type = *args
|
84
|
+
|
85
|
+
@body.form_param = form_param if form_param
|
86
|
+
@body.content_type = content_type if content_type
|
87
|
+
|
88
|
+
if File.exists?(content)
|
89
|
+
@body.content = File.new(content)
|
90
|
+
@body.content_type ||= "application/x-www-form-urlencoded"
|
91
|
+
else
|
92
|
+
@body.content = content
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
def inspect
|
98
|
+
str = "%s %s HTTP/%s" % [http_method, uri.to_s, http_version]
|
99
|
+
#str << $/ + headers.join($/) unless headers.empty?
|
100
|
+
#str << $/ + $/ + body.to_s if body
|
101
|
+
str
|
102
|
+
end
|
103
|
+
|
104
|
+
def to_s
|
105
|
+
str = "%s %s HTTP/%s" % [http_method, uri.to_s, http_version]
|
106
|
+
str
|
107
|
+
end
|
108
|
+
|
109
|
+
def cookies
|
110
|
+
return [] if !header.is_a?(Hash) || header[:Cookie].empty?
|
111
|
+
header[:Cookie]
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Stella::Data::HTTP
|
4
|
+
|
5
|
+
class Response < Storable
|
6
|
+
include Gibbler::Complex
|
7
|
+
|
8
|
+
attr_reader :raw_data
|
9
|
+
|
10
|
+
field :time => DateTime
|
11
|
+
field :client_ip => String
|
12
|
+
field :server_ip => String
|
13
|
+
field :header => String
|
14
|
+
field :body => String
|
15
|
+
field :status => String
|
16
|
+
field :message => String
|
17
|
+
field :http_version => String
|
18
|
+
|
19
|
+
def initialize(raw_data=nil)
|
20
|
+
@raw_data = raw_data
|
21
|
+
parse(@raw_data)
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse(raw)
|
25
|
+
return unless raw
|
26
|
+
@status, @http_version, @message, @header, @body = HTTPUtil::parse_http_response(raw)
|
27
|
+
end
|
28
|
+
|
29
|
+
def has_body?
|
30
|
+
!@body.nil? && !@body.empty?
|
31
|
+
end
|
32
|
+
def has_request?
|
33
|
+
false
|
34
|
+
end
|
35
|
+
def has_response?
|
36
|
+
false
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def body
|
41
|
+
return nil unless @body
|
42
|
+
#TODO: Move to HTTPResponse::Body.to_s
|
43
|
+
if is_binary?
|
44
|
+
"[skipping binary content]"
|
45
|
+
elsif is_gzip?
|
46
|
+
#require 'zlib'
|
47
|
+
#Zlib::Inflate.inflate(@body)
|
48
|
+
"[skipping gzip content]"
|
49
|
+
else
|
50
|
+
@body
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def headers
|
55
|
+
headers = []
|
56
|
+
header.each_pair do |n,v|
|
57
|
+
headers << [n.to_s.gsub('_', '-'), v[0]]
|
58
|
+
end
|
59
|
+
headers
|
60
|
+
end
|
61
|
+
|
62
|
+
def is_binary?
|
63
|
+
(!is_text?) == true
|
64
|
+
end
|
65
|
+
|
66
|
+
def is_text?
|
67
|
+
(!header[:Content_Type].nil? && (header[:Content_Type][0].is_a? String) && header[:Content_Type][0][/text/] != nil)
|
68
|
+
end
|
69
|
+
|
70
|
+
def is_gzip?
|
71
|
+
(!header[:Content_Encoding].nil? && (header[:Content_Encoding][0].is_a? String) && header[:Content_Encoding][0][/gzip/] != nil)
|
72
|
+
end
|
73
|
+
|
74
|
+
def inspect
|
75
|
+
str = "HTTP/%s %s (%s)" % [@http_version, @status, @message]
|
76
|
+
str << $/ + headers.join($/)
|
77
|
+
str << $/ + $/ + body if body
|
78
|
+
str
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_s
|
82
|
+
str = "%s: HTTP/%s %s (%s)" % [time.strftime(NICE_TIME_FORMAT), @http_version, @status, @message]
|
83
|
+
str
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
def cookies
|
88
|
+
return [] unless header.is_a?(Array) && !header[:Set_Cookie].empty?
|
89
|
+
header[:Set_Cookie]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|