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.
Files changed (55) hide show
  1. data/CHANGES.txt +7 -15
  2. data/LICENSE.txt +1 -1
  3. data/README.rdoc +93 -63
  4. data/Rakefile +32 -42
  5. data/bin/stella +138 -0
  6. data/examples/basic/listing_ids.csv +7 -0
  7. data/examples/basic/plan.rb +71 -0
  8. data/lib/stella/cli.rb +66 -0
  9. data/lib/stella/client.rb +199 -0
  10. data/lib/stella/config.rb +87 -0
  11. data/lib/stella/data/http/body.rb +15 -0
  12. data/lib/stella/data/http/request.rb +116 -0
  13. data/lib/stella/data/http/response.rb +92 -0
  14. data/lib/stella/data/http.rb +2 -257
  15. data/lib/stella/data.rb +85 -0
  16. data/lib/stella/dsl.rb +5 -0
  17. data/lib/stella/engine/functional.rb +39 -0
  18. data/lib/stella/engine/load.rb +106 -0
  19. data/lib/stella/engine.rb +55 -0
  20. data/lib/stella/exceptions.rb +15 -0
  21. data/lib/stella/guidelines.rb +18 -0
  22. data/lib/stella/mixins.rb +2 -0
  23. data/lib/stella/stats.rb +3 -7
  24. data/lib/stella/testplan/stats.rb +26 -0
  25. data/lib/stella/testplan/usecase.rb +67 -0
  26. data/lib/stella/testplan.rb +95 -220
  27. data/lib/{util → stella/utils}/httputil.rb +0 -0
  28. data/lib/stella/utils.rb +126 -0
  29. data/lib/stella/version.rb +15 -0
  30. data/lib/stella.rb +58 -104
  31. data/lib/threadify.rb +0 -6
  32. data/stella.gemspec +43 -49
  33. data/support/example_webapp.rb +246 -0
  34. data/support/useragents.txt +75 -0
  35. metadata +68 -32
  36. data/bin/example_test.rb +0 -82
  37. data/bin/example_webapp.rb +0 -63
  38. data/lib/logger.rb +0 -79
  39. data/lib/stella/clients.rb +0 -161
  40. data/lib/stella/command/base.rb +0 -20
  41. data/lib/stella/command/form.rb +0 -36
  42. data/lib/stella/command/get.rb +0 -44
  43. data/lib/stella/common.rb +0 -53
  44. data/lib/stella/crypto.rb +0 -88
  45. data/lib/stella/data/domain.rb +0 -82
  46. data/lib/stella/environment.rb +0 -66
  47. data/lib/stella/functest.rb +0 -105
  48. data/lib/stella/loadtest.rb +0 -186
  49. data/lib/stella/testrunner.rb +0 -64
  50. data/lib/storable.rb +0 -280
  51. data/lib/timeunits.rb +0 -65
  52. data/tryouts/drb/drb_test.rb +0 -65
  53. data/tryouts/drb/open4.rb +0 -19
  54. data/tryouts/drb/slave.rb +0 -27
  55. 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,15 @@
1
+ module Stella::Data::HTTP
2
+ class Body < Storable
3
+ include Gibbler::Complex
4
+
5
+ field :content_type
6
+ field :form_param
7
+ field :content
8
+
9
+ def has_content?
10
+ !@content.nil?
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -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