stella 0.8.8.001 → 2.0.1.001

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data/CHANGES.txt +9 -1
  2. data/Gemfile +19 -0
  3. data/Gemfile.lock +50 -0
  4. data/README.md +5 -79
  5. data/Rakefile +10 -7
  6. data/Rudyfile +1 -1
  7. data/TODO +31 -0
  8. data/VERSION.yml +3 -4
  9. data/bin/stella +23 -81
  10. data/certs/README.txt +17 -0
  11. data/certs/cacerts.pem +1529 -0
  12. data/certs/gd-class2-root.crt +24 -0
  13. data/certs/gd_bundle.crt +76 -0
  14. data/certs/gd_intermediate.crt +29 -0
  15. data/certs/startssl-ca.pem +44 -0
  16. data/certs/startssl-sub.class1.server.ca.pem +36 -0
  17. data/certs/stella-master.crt +1738 -0
  18. data/lib/stella.rb +191 -123
  19. data/lib/stella/cli.rb +47 -67
  20. data/lib/stella/client.rb +424 -360
  21. data/lib/stella/core_ext.rb +527 -0
  22. data/lib/stella/engine.rb +126 -419
  23. data/lib/stella/report.rb +391 -0
  24. data/lib/stella/testplan.rb +432 -306
  25. data/lib/stella/utils.rb +227 -2
  26. data/stella.gemspec +56 -55
  27. data/try/00_basics_try.rb +29 -0
  28. data/try/01_selectable_try.rb +25 -0
  29. data/try/09_utils_try.rb +67 -0
  30. data/try/10_stella_object_try.rb +49 -0
  31. data/try/40_report_try.rb +133 -0
  32. data/try/90_class_syntax_try.rb +13 -0
  33. data/try/emhttp.rb +62 -0
  34. data/try/rubyroute.rb +70 -0
  35. data/try/support/file.bmp +0 -0
  36. data/try/support/file.gif +0 -0
  37. data/try/support/file.ico +0 -0
  38. data/try/support/file.jpeg +0 -0
  39. data/try/support/file.jpg +0 -0
  40. data/try/support/file.png +0 -0
  41. data/try/traceviz.rb +60 -0
  42. data/vendor/httpclient-2.1.5.2/httpclient/session.rb +5 -2
  43. metadata +81 -53
  44. data/examples/cookies/plan.rb +0 -49
  45. data/examples/csvdata/plan.rb +0 -32
  46. data/examples/csvdata/search_terms.csv +0 -14
  47. data/examples/dynamic/plan.rb +0 -60
  48. data/examples/essentials/logo.png +0 -0
  49. data/examples/essentials/plan.rb +0 -248
  50. data/examples/essentials/search_terms.txt +0 -19
  51. data/examples/exceptions/plan.rb +0 -20
  52. data/examples/httpauth/plan.rb +0 -33
  53. data/examples/timeout/plan.rb +0 -18
  54. data/examples/variables/plan.rb +0 -41
  55. data/lib/stella/client/container.rb +0 -378
  56. data/lib/stella/common.rb +0 -363
  57. data/lib/stella/data.rb +0 -59
  58. data/lib/stella/data/http.rb +0 -189
  59. data/lib/stella/engine/functional.rb +0 -156
  60. data/lib/stella/engine/load.rb +0 -516
  61. data/lib/stella/guidelines.rb +0 -18
  62. data/lib/stella/logger.rb +0 -150
  63. data/lib/stella/utils/httputil.rb +0 -266
  64. data/try/01_numeric_mixins_tryouts.rb +0 -40
  65. data/try/12_digest_tryouts.rb +0 -42
  66. data/try/70_module_usage.rb +0 -21
  67. data/try/api/10_functional.rb +0 -20
  68. data/try/configs/failed_requests.rb +0 -31
  69. data/try/configs/global_sequential.rb +0 -18
  70. data/try/proofs/thread_queue.rb +0 -21
data/lib/stella/client.rb CHANGED
@@ -1,444 +1,508 @@
1
- require "observer"
2
- require "nokogiri"
3
- require 'pp'
1
+
2
+ require 'base64'
3
+ require 'addressable/uri'
4
4
 
5
5
  Stella::Utils.require_vendor "httpclient", '2.1.5.2'
6
6
 
7
- module Stella
7
+ require 'pp'
8
+
9
+ class Stella
10
+
8
11
  class Client
9
- MAX_REDIRECTS = 5.freeze unless defined?(MAX_REDIRECTS)
10
-
11
- require 'stella/client/container'
12
+ unless defined?(SSL_CERT_PATH)
13
+ SSL_CERT_PATH = File.join(STELLA_LIB_HOME, '..', 'certs', 'stella-master.crt').freeze
14
+ end
12
15
 
13
16
  include Gibbler::Complex
14
- include Observable
17
+ include HTTPClient::Timeout
15
18
 
16
19
  attr_reader :index
17
20
  attr_accessor :base_uri
18
21
  attr_accessor :proxy
19
22
  attr_accessor :created
23
+ attr_reader :clientid
24
+
25
+ gibbler :index, :opts, :base_uri, :proxy, :created
20
26
 
21
- gibbler :opts, :index, :base_uri, :proxy, :nowait, :created
22
27
  @@client_index = 0
23
- def initialize(base_uri=nil, opts={})
28
+
29
+ # Options:
30
+ #
31
+ # * :timeout (Integer) => 30
32
+ # * :ssl_verify_mode (Class) => nil (possible values: OpenSSL::SSL::VERIFY_NONE)
33
+ #
34
+ def initialize(opts={})
24
35
  @index = @@client_index += 1
25
- @created = Time.now.to_f
26
- opts = {
27
- :nowait => false,
28
- :withparam => false,
29
- :withheader => false,
30
- :notemplates => false,
31
- :wait => 0,
32
- :timeout => nil
33
- }.merge! opts
36
+ @created = Stella.now
34
37
  @opts = opts
35
- @base_uri, @index = base_uri, index
36
- #@cookie_file = File.new("cookies-#{index}", 'w')
38
+ @opts[:timeout] ||= 30
39
+ @base_uri, @index = opts[:base_uri] || opts['base_uri'], index
37
40
  @proxy = OpenStruct.new
41
+ @done = false
42
+ @session = Session.new @base_uri
43
+ @redirect_count = 0
44
+ @clientid = [@session.object_id, created, index, opts].digest
38
45
  end
39
- def id
40
- @id || self.digest
41
- end
42
- def execute(usecase, &stat_collector)
43
- # Gibbler.enable_debug
44
- # We need to make sure the digest cache has a value
45
- @id = self.digest if self.digest_cache.nil?
46
- Gibbler.disable_debug
47
- http_client = create_http_client
48
- stats = {}
49
- container = Container.new(self.id, usecase)
50
- counter = 0
51
- usecase.requests.each do |req|
52
- counter += 1
53
-
54
- container.reset_temp_vars
55
-
56
- stats ||= Benelux::Stats.new
57
- update(:prepare_request, usecase, req, counter)
58
-
59
- begin
60
- # This is for the values that were "set"
61
- # in the part before the response body.
62
- prepare_resources(container, req.resources)
46
+
47
+ def exception
48
+ @session.exception
49
+ end
50
+
51
+ def execute usecase, &each_request
52
+ @session.http_client = create_http_client
53
+ tt = Benelux.current_track.timeline
54
+ usecase.requests.each_with_index do |rt,idx|
55
+ begin
56
+ debug "request start (session: #{@session.object_id})"
57
+ @session.prepare_request usecase, rt
63
58
 
64
- params = prepare_params(container, req.params)
65
- headers = prepare_headers(container, req.headers)
59
+ debug "#{@session.http_method} #{@session.uri} (#{rt.id.short})"
60
+ debug " #{@session.params.inspect}" unless @session.params.empty?
61
+ debug " #{@session.headers.inspect}" unless @session.headers.empty?
66
62
 
67
- container.params, container.headers = params, headers
63
+ stella_id = [clientid, rt.id, @session.uri.to_s, @session.params, @session.headers, idx].digest
68
64
 
69
- uri = build_request_uri req.uri, params, container
70
-
71
- if http_auth = req.http_auth || usecase.http_auth
72
- # TODO: The first arg is domain and can include a URI path.
73
- # Are there cases where this is important?
74
-
75
- domain = http_auth.domain
76
- # When req.uri is a fully qualified URI, domain will be
77
- # set to an incorrect value like, http://domain1/http://domain2.
78
- # So we parse it and if that fails we'll set it to the value given.
79
- uri_tmp = URI.parse(req.uri).uri rescue req.uri
80
- domain ||= '%s://%s:%d%s' % [uri.scheme, uri.host, uri.port, '/']
81
- domain = container.instance_eval &domain if Proc === domain
82
- Stella.ld "DOMAIN " << domain
83
- user, pass = http_auth.user, http_auth.pass
84
- user = container.instance_eval &user if Proc === user
85
- pass = container.instance_eval &pass if Proc === pass
86
- update(:authenticate, usecase, req, domain, user, pass)
87
- http_client.set_auth(domain, user, pass)
88
- end
65
+ Benelux.current_track.add_tags :request => rt.id
66
+ Benelux.current_track.add_tags :stella_id => stella_id
89
67
 
90
- if tout = req.timeout || usecase.timeout || @opts[:timeout]
91
- http_client.receive_timeout = tout if tout > 0
92
- end
93
- Stella.ld "TIMEOUT " << http_client.receive_timeout.to_s
68
+ ## Useful for testing larger large request header
69
+ ## 50.times do |idx|
70
+ ## headers["X-header-#{idx}"] = (1000 << 1000).to_s
71
+ ## end
94
72
 
95
- raise NoHostDefined, req.uri if uri.host.nil? || uri.host.empty?
96
- stella_id = [Time.now.to_f, self.id, req.id, params, headers, counter].digest
97
-
98
- Benelux.add_thread_tags :request => req.id
99
- Benelux.add_thread_tags :retry => counter
100
- Benelux.add_thread_tags :stella_id => stella_id
73
+ # Mis-behaving HTTP servers will fail w/o an Accept header
74
+ @session.headers["Accept"] ||= '*/*'
101
75
 
102
- container.unique_id = stella_id
76
+ # if hard_timeout is nil this will do nothing
77
+ timeout(@opts[:hard_timeout], TimeoutError) do
78
+ @session.generate_request stella_id
79
+ end
80
+ res = @session.res
103
81
 
104
- params['__stella'] = container.unique_id.short if @opts[:'withparam']
105
- headers['X-Stella-ID'] = container.unique_id.short if @opts[:'withheader']
82
+ each_request.call(@session) unless each_request.nil?
106
83
 
107
- meth = req.http_method.to_s.downcase
108
- Stella.ld "#{req.http_method}: " << "#{req.uri} " << params.inspect
109
-
110
- ret, asset_duration = nil, 0
111
- rescue => ex
112
- update(:request_unhandled_exception, usecase, uri, req, params, ex)
113
- Benelux.remove_thread_tags :status, :retry, :request, :stella_id
114
- break
115
- end
116
-
117
-
118
-
119
- begin
84
+ # Needs to happen before handle response incase it raises an exception
85
+ log = Stella::Log::HTTP.new Stella.now,
86
+ @session.http_method, @session.uri, @session.params, res.request.header.dump,
87
+ res.request.body.content, res.status, res.header.dump, res.body.content
120
88
 
121
- send_request http_client, usecase, meth, uri, req, params, headers, container, counter
89
+ tt.add_count :requests, 1, :kind => :http
122
90
 
123
- update(:receive_response, usecase, uri, req, params, headers, counter, container)
91
+ run_sleeper @opts[:wait]
124
92
 
125
- Benelux.add_thread_tags :status => container.status
126
- res = container.response
127
- [
128
- [:request_header_size, res.request.header.dump.size],
129
- [:request_content_size, res.request.body.content.size],
130
- [:response_headers_size, res.header.dump.size],
131
- [:response_content_size, res.body.content.size]
132
- ].each do |att|
133
- Benelux.thread_timeline.add_count att[0], att[1]
93
+ if @session.response_handler?
94
+ @session.handle_response
95
+ elsif res.status >= 400
96
+ raise Stella::HTTPError.new(res.status)
97
+ elsif rt.follow && @session.redirect?
98
+ raise ForcedRedirect, @session.location
134
99
  end
100
+
101
+ tt.add_message log, :status => res.status, :kind => :http_log, :state => :nominal
135
102
 
103
+ @redirect_count = 0
136
104
 
137
- ret = execute_response_handler container, req
105
+ rescue RepeatRequest => ex
106
+ debug " REPEAT REQUEST: #{@session.location}"
107
+ retry
138
108
 
139
- asset_start = Time.now
140
- container.assets.each do |uri|
141
- Benelux.add_thread_tags :asset => uri
142
- a = http_client.get uri
143
- Stella.stdout.info3 " FETCH ASSET: #{uri} #{a.status}"
144
- Benelux.remove_thread_tags :asset
109
+ rescue ForcedRedirect => ex
110
+ # TODO: warn when redirecting from https to http
111
+ debug " FOUND REDIRECT: #{@session.location}"
112
+ if @redirect_count < 10
113
+ @redirect_count += 1
114
+ @session.clear_previous_request
115
+ @session.redirect_uri = ex.location
116
+ retry
145
117
  end
146
- asset_duration = Time.now - asset_start
147
- rescue HTTPClient::ConnectTimeoutError, HTTPClient::SendTimeoutError,
148
- Errno::ECONNRESET, HTTPClient::ReceiveTimeoutError => ex
149
- update(:request_timeout, usecase, uri, req, params, headers, counter, container, http_client.receive_timeout)
150
- Benelux.remove_thread_tags :status, :retry, :request, :stella_id
151
- next
152
- rescue => ex
153
- update(:request_unhandled_exception, usecase, uri, req, params, ex)
154
- Benelux.remove_thread_tags :status, :retry, :request, :stella_id
155
- next
156
- end
157
118
 
158
- if (req.wait.is_a?(::Range) && req.wait.last > 0) ||
159
- (Numeric === req.wait && req.wait > 0)
160
- wait = req.wait
161
- else
162
- wait = @opts[:wait]
163
- end
164
-
165
- run_sleeper(wait, asset_duration) unless nowait?
166
-
167
-
168
- # TODO: consider throw/catch
169
- case ret.class.to_s
170
- when "Stella::Client::Repeat"
171
- update(:request_repeat, counter, ret.times+1, uri, container)
172
- Benelux.remove_thread_tags :status
173
- redo if counter <= ret.times
174
- when "Stella::Client::Follow"
175
- ret.uri ||= container.header['Location'].first
176
- if counter > MAX_REDIRECTS
177
- update(:max_redirects, counter-1, ret, uri, container)
178
- break
179
- else
180
- req = ret.generate_request(req)
181
- update(:follow_redirect, ret, uri, container)
182
- Benelux.remove_thread_tags :status, :request
183
- redo
119
+ rescue Errno::ETIMEDOUT, SocketError,
120
+ HTTPClient::ConnectTimeoutError,
121
+ HTTPClient::SendTimeoutError,
122
+ HTTPClient::ReceiveTimeoutError,
123
+ TimeoutError,
124
+ Errno::ECONNRESET => ex
125
+ debug "[#{ex.class}] #{ex.message}"
126
+ log = Stella::Log::HTTP.new Stella.now, @session.http_method, @session.uri, @session.params
127
+ if @session.res
128
+ log.request_headers = @session.res.request.header.dump if @session.res.request
129
+ log.request_body = @session.res.request.body.content if @session.res.request
130
+ log.response_status = @session.res.status
131
+ log.response_headers = @session.res.header.dump if @session.res.content
132
+ log.response_body = @session.res.body.content if @session.res.body
184
133
  end
185
- when "Stella::Client::Quit"
186
- update(:usecase_quit, ret.message, uri, container)
187
- Benelux.remove_thread_tags :status
134
+ log.msg = "#{ex.class} (#{@session.http_client.receive_timeout})"
135
+ tt.add_message log, :kind => :http_log, :state => :timeout
136
+ Benelux.current_track.remove_tags :status, :request, :stella_id
137
+ next
138
+
139
+ rescue StellaError, StellaBehavior => ex
140
+ debug "[#{ex.class}] #{ex.message}"
141
+ log = Stella::Log::HTTP.new Stella.now, @session.http_method, @session.uri, @session.params
142
+ if @session.res
143
+ log.request_headers = @session.res.request.header.dump if @session.res.request
144
+ log.request_body = @session.res.request.body.content if @session.res.request
145
+ log.response_status = @session.res.status
146
+ log.response_headers = @session.res.header.dump if @session.res.content
147
+ log.response_body = @session.res.body.content if @session.res.body
148
+ end
149
+ log.msg = ex.message
150
+ tt.add_message log, :status => log.response_status, :kind => :http_log, :state => :exception
151
+ Benelux.current_track.remove_tags :status, :request, :stella_id
152
+ @session.exception = ex
188
153
  break
189
- when "Stella::Client::Fail"
190
- update(:request_fail, ret.message, uri, container)
191
- when "Stella::Client::Error"
192
- update(:request_error, ret.message, uri, container)
193
- end
194
154
 
195
- Benelux.remove_thread_tags :status
155
+ rescue Errno::ECONNREFUSED => ex
156
+ debug "[#{ex.class}] #{ex.message}"
157
+ log = Stella::Log::HTTP.new Stella.now, @session.http_method, @session.uri, @session.params
158
+ log.msg = "Connection refused"
159
+ tt.add_message log, :status => log.response_status, :kind => :http_log, :state => :exception
160
+ Benelux.current_track.remove_tags :status, :request, :stella_id
161
+ break
196
162
 
197
- counter = 0 # reset
163
+ rescue OpenSSL::SSL::SSLError => ex
164
+ debug "[#{ex.class}] #{ex.message}"
165
+ log = Stella::Log::HTTP.new Stella.now, @session.http_method, @session.uri, @session.params
166
+ log.msg = ex.message
167
+ tt.add_message log, :status => log.response_status, :kind => :http_log, :state => :exception
168
+ Benelux.current_track.remove_tags :status, :request, :stella_id
169
+ break
170
+
171
+ rescue => ex
172
+ Stella.le "[#{ex.class}] #{ex.message}", ex.backtrace
173
+ log = Stella::Log::HTTP.new Stella.now, @session.http_method, @session.uri, @session.params
174
+ log.msg = ex.message
175
+ tt.add_message log, :status => log.response_status, :kind => :http_log, :state => :fubar
176
+ Benelux.current_track.remove_tags :status, :request, :stella_id
177
+ break
178
+
179
+ end
198
180
  end
199
- Benelux.remove_thread_tags :retry, :request, :stella_id
200
- stats
201
- end
202
-
203
- def enable_nowait_mode; @opts[:'nowait'] = true; end
204
- def disable_nowait_mode; @opts[:'nowait'] = false; end
205
- def nowait?; @opts[:'nowait'] == true; end
206
181
 
207
- private
208
- # We use a method so we can time it with Benelux
209
- def send_request(http_client, usecase, meth, uri, req, params, headers, container, counter)
210
- if meth == "delete"
211
- args = [meth, uri, headers]
212
- else
213
- args = [meth, uri, params, headers]
214
- end
215
- container.response = http_client.send(*args) # booya!
216
182
  end
217
183
 
218
- def update(kind, *args)
219
- changed and notify_observers(kind, self.id, *args)
184
+ def run_sleeper dur
185
+ return unless dur && dur > 0
186
+ dur = (rand * (dur.last-dur.first) + dur.first) if Range === dur
187
+ debug "sleep: #{dur}"
188
+ sleep dur
220
189
  end
221
-
222
- def run_sleeper(wait, already_waited=0)
223
- return if (wait.is_a?(::Range) && wait.last == 0) || wait == 0
224
- # The time it took to download the assets can
225
- # be removed from the specified wait time.
226
- if wait.is_a?(::Range)
227
- ms = rand(wait.last * 1000).to_f
228
- ms = wait.first if ms < wait.first
229
- else
230
- ms = wait * 1000
231
- end
232
- Stella.stdout.info3 " WAIT: #{ms}ms"
233
- sec = ms / 1000
234
- Stella.ld "WAIT ADJUSTED FROM %.1f TO: %.1f" % [sec, (sec - already_waited)]
235
- sleep (sec - already_waited) if (sec - already_waited) > 0
190
+
191
+ def debug(msg)
192
+ Stella.ld " #{clientid.short} #{msg}"
236
193
  end
237
194
 
238
195
  def create_http_client
239
- opts = {
240
- :proxy => @proxy.uri || nil, # a tautology for clarity
241
- :agent_name => Stella.agent,
196
+ http_client = HTTPClient.new(
197
+ :agent_name => @opts[:agent] || @opts['agent'] || Stella.agent,
242
198
  :from => nil
243
- }
244
- http_client = HTTPClient.new opts
245
- http_client.set_proxy_auth(@proxy.user, @proxy.pass) if @proxy.user
246
- http_client.debug_dev = STDOUT if Stella.debug? && Stella.stdout.lev >= 3
199
+ )
200
+ #http_client.set_proxy_auth(@proxy.user, @proxy.pass) if @proxy.user
201
+ #http_client.debug_dev = STDOUT if Stella.debug?
247
202
  http_client.protocol_version = "HTTP/1.1"
248
- http_client.ssl_config.verify_mode = ::OpenSSL::SSL::VERIFY_NONE
203
+ if @opts[:ssl_verify_mode]
204
+ http_client.ssl_config.verify_mode = @opts[:ssl_verify_mode]
205
+ end
206
+
207
+ # See: http://ghouston.blogspot.com/2006/03/using-ssl-with-ruby-http-access2.html
208
+ begin
209
+ http_client.ssl_config.clear_cert_store
210
+ http_client.ssl_config.set_trust_ca SSL_CERT_PATH
211
+ rescue => ex
212
+ Stella.li ex.class, ex.message
213
+ Stella.ld ex.backtrace
214
+ end
215
+
216
+ http_client.connect_timeout = @opts[:timeout]
217
+ http_client.send_timeout = @opts[:timeout]
218
+ http_client.receive_timeout = @opts[:timeout]
249
219
  http_client
250
220
  end
251
221
 
252
- def prepare_resources(container, resources)
253
- h = prepare_runtime_hash container, resources
254
- # p [container.client_id.shorter, h]
255
- container.resources.merge! h
222
+ def done!
223
+ @done = true
256
224
  end
257
225
 
258
- # Process resource values from the request object
259
- def prepare_runtime_hash(container, hashobj, &extra)
260
- newh = {}
261
- #Stella.ld "PREPARE HEADERS: #{headers}"
262
- hashobj.each_pair do |n,v|
263
- unless @opts[:'notemplates']
264
- v = container.parse_template v
226
+ def done?
227
+ @done == true
228
+ end
229
+
230
+ end
231
+
232
+ class Session
233
+ attr_reader :events, :response_handler, :res, :req, :rt, :vars, :previous_doc, :http_auth
234
+ attr_accessor :headers, :params, :base_uri, :http_client, :uri, :redirect_uri, :http_method, :exception
235
+ def initialize(base_uri=nil)
236
+ @base_uri = base_uri
237
+ @vars = indifferent_hash
238
+ @base_uri &&= Addressable::URI.parse(@base_uri) if String === @base_uri
239
+ @events = SelectableArray.new
240
+ end
241
+ def current_event
242
+ @events.last
243
+ end
244
+ alias_method :param, :params
245
+ alias_method :header, :headers
246
+ def prepare_request uc, rt
247
+ clear_previous_request
248
+ @rt = rt
249
+ @vars.merge! uc.class.session || {}
250
+ registered_classes = []
251
+ if uc.class.testplan
252
+ @vars.merge! uc.class.testplan.class.session || {}
253
+ registered_classes = uc.class.testplan.class.registered_classes || []
254
+ end
255
+ registered_classes.push *(uc.class.registered_classes || [])
256
+ registered_classes.each do |klass|
257
+ self.extend klass unless self.kind_of?(klass)
258
+ end
259
+ @http_method, @params, @headers = rt.http_method, rt.params, rt.headers
260
+ @http_auth = uc.http_auth
261
+ instance_exec(&rt.callback) unless rt.callback.nil?
262
+ @uri = if @redirect_uri
263
+ @params = {}
264
+ @headers = {}
265
+ @http_method = :get
266
+ if @redirect_uri.scheme
267
+ tmp = [@redirect_uri.scheme, '://', @redirect_uri.host].join
268
+ tmp << ":#{@redirect_uri.port}" unless [80,443].member?(@redirect_uri.port)
269
+ @base_uri = Addressable::URI.parse(tmp)
265
270
  end
266
- v = extra.call(v) unless extra.nil?
267
- newh[n] = v
271
+ build_uri @redirect_uri
272
+ else
273
+ build_uri @rt.uri
268
274
  end
269
- newh
275
+ if !http_auth.nil? && !http_auth.empty?
276
+ Stella.ld " HTTP AUTH: #{http_auth.inspect}"
277
+ http_auth[:domain] ||= '%s://%s:%d%s' % [base_uri.scheme, base_uri.host, base_uri.port, '/']
278
+ http_client.set_auth http_auth[:domain], http_auth[:user], http_auth[:pass]
279
+ Stella.ld " #{http_client.www_auth.inspect}"
280
+ end
281
+ @redirect_uri = nil # one time deal
270
282
  end
271
- alias_method :prepare_headers, :prepare_runtime_hash
272
- alias_method :prepare_params, :prepare_runtime_hash
273
-
274
- # Testplan URIs can be relative or absolute. Either one can
275
- # contain variables in the form <tt>:varname</tt>, as in:
276
- #
277
- # http://example.com/product/:productid
278
- #
279
- # This method creates a new URI object using the @base_uri
280
- # if necessary and replaces all variables with literal values.
281
- # If no replacement value can be found, the variable will remain.
282
- def build_request_uri(uri, params, container)
283
- raise "Request given with no URI" if uri.nil?
284
- newuri = uri.clone # don't modify uri template
285
- # We call uri.clone b/c we modify uri.
286
- uri.scan(/([:\$])([a-z_]+)/i) do |inst|
287
- val = find_replacement_value(inst[1], params, container, base_uri)
288
- Stella.ld "FOUND VAR: #{inst[0]}#{inst[1]} (value: #{val})"
289
- re = Regexp.new "\\#{inst[0]}#{inst[1]}"
290
- newuri.gsub! re, val.to_s unless val.nil?
283
+ def generate_request(event_id)
284
+ @res = http_client.send(@http_method.to_s.downcase, @uri, params, headers)
285
+ @req = @res.request
286
+ @events << event_id
287
+ @res
288
+ end
289
+ def location
290
+ @location ||= Addressable::URI.parse(@res.header['location'].first || '')
291
+ @location
292
+ end
293
+ def redirect?
294
+ @res && (300..399).member?(@res.status)
295
+ end
296
+ def doc
297
+ return @doc unless @doc.nil?
298
+ return nil if @res.content.nil? || @res.content.empty?
299
+ str = RUBY_VERSION >= "1.9.0" ? @res.content.force_encoding("UTF-8") : @res.content
300
+ # NOTE: It's important to parse the document on every
301
+ # request because this container is available for the
302
+ # entire life of a usecase.
303
+ @doc = case (@res.header['Content-Type'] || []).first
304
+ when /text\/html/
305
+ Nokogiri::HTML(str)
306
+ when /text\/xml/
307
+ Nokogiri::XML(str)
308
+ when /text\/yaml/
309
+ YAML.load(str)
310
+ when /application\/json/
311
+ Yajl::Parser.parse(str)
291
312
  end
292
-
293
- uri = URI.parse(newuri)
294
-
295
- if uri.host.nil? && base_uri.nil?
296
- Stella.abort!
297
- raise NoHostDefined, uri
313
+ @doc.replace indifferent_params(@doc) if Hash === @doc
314
+ @doc
315
+ end
316
+ def form
317
+ return @form unless @form.nil?
318
+ return nil if doc.nil?
319
+ return nil unless content_type?('text/html')
320
+ @form = indifferent_hash
321
+ forms = doc.css('form')
322
+ forms.each_with_index do |html,idx|
323
+ name = html['id'] || html['name'] || html['class']
324
+ Stella.ld [:form, idx, name].inspect
325
+ form = indifferent_hash
326
+ # Store form attributes in keys prefixed with an underscore.
327
+ html.each { |att,val| form["_#{att}"] = val }
328
+ # Store input name and values in the form hash.
329
+ html.css('input').each do |input|
330
+ form[input['name']] = input['value']
331
+ end
332
+ # Store the form by the name and index in the document
333
+ @form[name] = @form[idx] = form
298
334
  end
299
-
300
- base_uri = URI.parse(self.base_uri || '')
301
- uri.scheme = base_uri.scheme if uri.scheme.nil?
302
- uri.host = base_uri.host if uri.host.nil?
303
- uri.port = base_uri.port if uri.port.nil?
304
- uri.port = 443 if uri.port == 80 && uri.scheme == "https"
305
-
306
- # Support for specifying default path prefix:
307
- # $ stella verify -p plan.rb http://localhost/basicauth
308
- if URI::Generic === base_uri && base_uri.path && uri != base_uri
309
- if uri.path.nil? || uri.path.empty?
310
- begin
311
- uri.path = base_uri.path
312
- rescue => ex
313
- Stella.stdout.info "#{ex.class}: #{self.base_uri}"
314
- exit
315
- end
316
- else
317
- a = base_uri.path.gsub(/\/$/, '')
318
- b = uri.path.gsub(/^\//, '')
319
- uri.path = [a,b].join('/')
335
+ @form
336
+ end
337
+ alias_method :forms, :form
338
+ def cookie
339
+ return @cookie unless @cookie.nil?
340
+ return nil unless http_client.cookie_manager
341
+ return nil if http_client.cookie_manager.cookies.empty?
342
+ @cookie = indifferent_hash
343
+ http_client.cookie_manager.cookies.each do |c|
344
+ next unless c.match?(uri)
345
+ @cookie[c.name] = c.value
346
+ end
347
+ @cookie
348
+ end
349
+ alias_method :cookies, :cookie
350
+ def content_type? guess
351
+ guess = Regexp.new guess unless Regexp === guess
352
+ guess.match((res.header['Content-Type'] || []).first)
353
+ end
354
+ def handle_response
355
+ return unless response_handler?
356
+ instance_exec(&find_response_handler(@res.status))
357
+ @previous_doc = doc
358
+ end
359
+ def find_response_handler(status)
360
+ return if response_handler.nil?
361
+ key = response_handler.keys.select { |range| range.member?(status) }.first
362
+ response_handler[key] if key
363
+ end
364
+ def response_handler?
365
+ status = (@res.status || 0).to_i
366
+ !find_response_handler(status).nil?
367
+ end
368
+ def response_handler(range=nil, &blk)
369
+ @response_handler ||= {}
370
+ return @response_handler if range.nil? && blk.nil?
371
+ range = 0..999 if range.nil? || range.zero?
372
+ range = range.to_i..range.to_i unless Range === range
373
+ @response_handler[range] = blk unless blk.nil?
374
+ @response_handler[range]
375
+ end
376
+ alias_method :handler, :response_handler
377
+ alias_method :response, :response_handler
378
+ alias_method :session, :vars
379
+ def clear_previous_request
380
+ [:doc, :location, :res, :req, :rt, :params, :headers, :cookie, :form, :response_handler, :http_method, :exception].each do |n|
381
+ instance_variable_set :"@#{n}", nil
382
+ end
383
+ end
384
+ def status
385
+ @res.status
386
+ end
387
+
388
+ def wait(t); sleep t; end
389
+ def quit(msg=nil); raise TestplanQuit.new(msg); end
390
+ def fail(msg=nil); raise UsecaseFail.new(msg); end
391
+ def error(msg=nil); raise RequestError.new(msg); end
392
+ def repeat(t=1); raise RepeatRequest.new(t); end
393
+ def follow(uri=nil,&blk); raise ForcedRedirect.new(uri,&blk); end
394
+
395
+ private
396
+ def build_uri(reqtempl)
397
+ uri = reqtempl.clone # need to clone b/c we modify uri in scan.
398
+ reqtempl.to_s.scan(/([:\$])([a-z_]+)/i) do |inst|
399
+ val = find_replacement_value(inst[1])
400
+ Stella.ld " FOUND VAR: #{inst[0]}#{inst[1]} (value: #{val})"
401
+ if val.nil?
402
+ raise Stella::UsecaseError, "no value for #{inst[0]}#{inst[1]} in '#{@rt.uri}'"
320
403
  end
404
+ re = Regexp.new "\\#{inst[0]}#{inst[1]}"
405
+ uri.gsub! re, val.to_s unless val.nil?
406
+ end
407
+ uri = Stella.canonical_uri(uri)
408
+ if base_uri
409
+ uri.scheme = base_uri.scheme
410
+ uri.host = base_uri.host if uri.host.to_s.empty?
411
+ uri.port = base_uri.port if uri.port.to_s.empty?
321
412
  end
322
-
323
413
  uri
324
414
  end
325
-
326
415
  # Testplan URIs can contain variables in the form <tt>:varname</tt>.
327
416
  # This method looks at the request parameters and then at the
328
417
  # usecase's resource hash for a replacement value.
329
418
  # If not found, returns nil.
330
- def find_replacement_value(name, params, container, base_uri)
331
- value = nil
332
- #Stella.ld "REPLACE: #{name}"
333
- #Stella.ld "PARAMS: #{params.inspect}"
334
- #Stella.ld "IVARS: #{container.instance_variables}"
335
- if name.to_sym == :HOSTNAME && !base_uri.nil?
336
- value = base_uri.host
337
- elsif params.has_key?(name.to_sym)
338
- value = params.delete name.to_sym
339
- elsif container.resource?( name)
340
- value = container.resource name
419
+ def find_replacement_value(name)
420
+ if @params.has_key?(name.to_sym)
421
+ @params.delete name.to_sym
422
+ elsif vars.has_key?(name.to_s) || vars.has_key?(name.to_s.to_sym)
423
+ vars[name.to_s] || vars[name.to_s.to_sym]
341
424
  elsif Stella::Testplan.global?(name)
342
425
  Stella::Testplan.global(name)
343
426
  end
344
- value
345
- end
427
+ end
428
+ def indifferent_params(params)
429
+ if params.is_a?(Hash)
430
+ params = indifferent_hash.merge(params)
431
+ params.each do |key, value|
432
+ next unless value.is_a?(Hash) || value.is_a?(Array)
433
+ params[key] = indifferent_params(value)
434
+ end
435
+ elsif params.is_a?(Array)
436
+ params.collect! do |value|
437
+ if value.is_a?(Hash) || value.is_a?(Array)
438
+ indifferent_params(value)
439
+ else
440
+ value
441
+ end
442
+ end
443
+ end
444
+ end
445
+
446
+ # Creates a Hash with indifferent access.
447
+ def indifferent_hash
448
+ Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
449
+ end
450
+ end
451
+
452
+ module Asserts
453
+ def assert_doc
454
+ fail 'No content' if doc.nil?
455
+ end
346
456
 
347
- # Find the appropriate response handler by executing the
348
- # HTTP response status against the configured handlers.
349
- # If several match, the first one is returned.
350
- def find_response_handler(container, req)
351
- handler = nil
352
- req.response.each_pair do |regex,h|
353
- Stella.ld "HANDLER REGEX: #{regex.to_s} (#{container.status})"
354
- regex = /#{regex}/ unless regex.is_a? Regexp
355
- handler = h and break if container.status.to_s =~ regex
457
+ def assert_keys expected_keys
458
+ assert_doc
459
+ found_keys = doc.keys.uniq.sort
460
+ unless found_keys == expected_keys
461
+ quit "Doc keys mismatch (#{found_keys})"
356
462
  end
357
- handler
358
463
  end
359
464
 
465
+ def assert_list key
466
+ assert_doc
467
+ fail "#{key} is empty" if doc[key].nil? || doc[key].empty?
468
+ end
360
469
 
361
- def execute_response_handler(container, req)
362
- ret = nil
363
- handler = find_response_handler container, req
364
- if handler.nil?
365
- if container.status >= 400
366
- update(:request_fail, "No handler", req.uri, container)
367
- end
368
- return
369
- end
370
- begin
371
- ret = container.instance_eval &handler
372
- update(:execute_response_handler, req, container)
373
- rescue => ex
374
- update(:error_execute_response_handler, ex, req, container)
375
- Stella.ld ex.message, ex.backtrace
470
+ def assert_object_keys key, expected_keys
471
+ assert_doc
472
+ found_keys = doc[key].collect { |obj| obj.keys }.flatten.uniq.sort
473
+ unless found_keys == expected_keys
474
+ quit "Doc keys mismatch (#{found_keys})"
376
475
  end
377
- ret
378
476
  end
379
477
 
380
- class ResponseError < Stella::Error
381
- def initialize(k, m=nil)
382
- @kind, @msg = k, m
383
- end
384
- def message
385
- msg = "#{@kind}"
386
- msg << ": #{@msg}" unless @msg.nil?
387
- msg
478
+ def assert_object_values key, object_key, expected_values
479
+ expected_values = [expected_values] unless Array === expected_values
480
+ expected_values = expected_values.collect { |v| v.to_s }.sort
481
+ values_found = doc[key].collect { |obj| obj[object_key].to_s }
482
+ values_found.sort!
483
+ unless values_found.uniq == expected_values
484
+ quit "#{key} contains unexpected values for #{object_key}: #{values_found.uniq}"
388
485
  end
389
486
  end
390
487
 
391
- end
392
- end
393
-
394
-
395
- class Stella::Client
396
-
397
- class ResponseModifier
398
- attr_accessor :obj
399
- def initialize(obj=nil)
400
- @obj = obj
401
- end
402
- alias_method :message, :obj
403
- alias_method :message=, :obj=
404
- end
405
- class Repeat < ResponseModifier;
406
- alias_method :times, :obj
407
- alias_method :times=, :obj=
408
- end
409
- #
410
- # Automatically follow a Location header or you
411
- # can optionally specify the URI to load.
412
- #
413
- # get '/' do
414
- # response 302 do
415
- # follow do
416
- # header :'X-SOME-HEADER' => 'somevalue'
417
- # end
418
- # end
419
- # end
420
- #
421
- # The block is optional and accepts the same syntax as regular requests.
422
- #
423
- class Follow < ResponseModifier;
424
- alias_method :uri, :obj
425
- alias_method :uri=, :obj=
426
- attr_reader :definition
427
- def initialize(obj=nil,&definition)
428
- @obj, @definition = obj, definition
429
- end
430
- def generate_request(req)
431
- n = Stella::Data::HTTP::Request.new :GET, self.uri, req.http_version, &definition
432
- n.description = "#{req.description || 'Request'} (autofollow)"
433
- n.http_auth = req.http_auth
434
- n.response_handler = req.response_handler
435
- n.autofollow!
436
- n.freeze
437
- n
488
+ def assert_form name
489
+ assert_doc
490
+ fail "No form called #{name}" unless form && form[name]
491
+ end
492
+ def assert_exists v
493
+ fail "Found nil value" if v.nil?
494
+ fail "Found empty value" if v.empty?
495
+ end
496
+ def assert_equals expected, found
497
+ fail "Expected: #{expected}; Found: #{found}" unless expected == found
498
+ end
499
+ def assert_matches regex, found
500
+ fail "Expected: #{regex}; Found: #{found}" unless regex.match(found)
501
+ end
502
+ alias_method :assert_match, :assert_matches
503
+ def assert_status expected
504
+ fail "Expected: #{expected}; Found: #{res.status}" unless res.status.to_i == expected.to_i
438
505
  end
439
506
  end
440
- class Quit < ResponseModifier; end
441
- class Fail < Quit; end
442
- class Error < Quit; end
443
507
 
444
508
  end