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
@@ -0,0 +1,391 @@
1
+ begin
2
+ require 'pismo'
3
+ rescue LoadError
4
+ end
5
+
6
+ class Stella
7
+ class Log
8
+ class HTTP < Storable
9
+ include Gibbler::Complex
10
+ include Selectable::Object
11
+ field :stamp
12
+ field :httpmethod
13
+ field :uri
14
+ field :request_params
15
+ field :request_headers
16
+ field :request_body
17
+ field :response_status
18
+ field :response_headers
19
+ field :response_body
20
+ field :msg
21
+ end
22
+ end
23
+ class Report < Storable
24
+ include Gibbler::Complex
25
+ CRLF = "\r\n" unless defined?(Report::CRLF)
26
+ @plugins, @plugin_order = {}, []
27
+ class << self
28
+ attr_reader :plugins, :plugin_order
29
+ def plugin?(name)
30
+ @plugins.has_key? name
31
+ end
32
+ def load(name)
33
+ @plugins[name]
34
+ end
35
+ end
36
+ module Plugin
37
+ def self.included(obj)
38
+ obj.extend ClassMethods
39
+ obj.field :processed => Boolean
40
+ end
41
+ def processed!
42
+ @processed = true
43
+ end
44
+ def processed?
45
+ @processed == true
46
+ end
47
+ attr_reader :report
48
+ def initialize(report=nil)
49
+ @report = report
50
+ end
51
+ module ClassMethods
52
+ attr_reader :plugin
53
+ def register(plugin)
54
+ @plugin = plugin
55
+ extra_methods = eval "#{self}::ReportMethods" rescue nil
56
+ Stella::Report.send(:include, extra_methods) if extra_methods
57
+ Stella::Report.field plugin => self
58
+ Stella::Report.plugins[plugin] = self
59
+ Stella::Report.plugin_order << plugin
60
+ end
61
+ def process *args
62
+ raise StellaError, "Must override run"
63
+ end
64
+ end
65
+ end
66
+
67
+ class Errors < Storable
68
+ include Gibbler::Complex
69
+ include Report::Plugin
70
+ field :exceptions
71
+ field :timeouts
72
+ field :fubars
73
+ def process(filter={})
74
+ @exceptions = report.timeline.messages.filter(:kind => :http_log, :state => :exception)
75
+ @timeouts = report.timeline.messages.filter(:kind => :http_log, :state => :timeout)
76
+ @fubars = report.timeline.messages.filter(:kind => :http_log, :state => :fubar)
77
+ processed!
78
+ end
79
+ def exceptions?
80
+ !@exceptions.nil? && !@exceptions.empty?
81
+ end
82
+ def timeouts?
83
+ !@timeouts.nil? && !@timeouts.empty?
84
+ end
85
+ def fubars?
86
+ !@fubars.nil? && !@fubars.empty?
87
+ end
88
+ def all
89
+ [@exceptions, @timeouts, @fubars].flatten
90
+ end
91
+ module ReportMethods
92
+ # expects Statuses plugin is loaded
93
+ def errors?
94
+ exceptions? || timeouts? || fubars? || (statuses && !statuses.nonsuccessful.empty?)
95
+ end
96
+ def exceptions?
97
+ return false unless processed? && errors
98
+ errors.exceptions?
99
+ end
100
+ def timeouts?
101
+ return false unless processed? && errors
102
+ errors.timeouts?
103
+ end
104
+ def error_count
105
+ errors.all.size
106
+ end
107
+ def fubars?
108
+ return false unless processed? && errors
109
+ errors.fubars?
110
+ end
111
+ end
112
+ register :errors
113
+ end
114
+
115
+ class Content < Storable
116
+ include Gibbler::Complex
117
+ include Report::Plugin
118
+ field :request_body
119
+ field :response_body
120
+ field :request_body_digest
121
+ field :response_body_digest
122
+ field :keywords => Array
123
+ field :title
124
+ field :favicon
125
+ field :author
126
+ field :lede
127
+ field :description
128
+ field :is_binary => Boolean
129
+ field :is_image => Boolean
130
+ field :log => Array
131
+ def binary?
132
+ @is_binary == true
133
+ end
134
+ def image?
135
+ @is_image == true
136
+ end
137
+ def process(filter={})
138
+ if report.errors.exceptions?
139
+ @log = report.errors.exceptions
140
+ elsif report.errors.fubars?
141
+ @log = report.errors.fubars
142
+ elsif report.errors.timeouts?
143
+ @log = report.errors.timeouts
144
+ else
145
+ @log = report.timeline.messages.filter(:kind => :http_log, :state => :nominal)
146
+ end
147
+
148
+ return if @log.empty?
149
+
150
+ unless Stella::Utils.binary?(@log.first.request_body) || Stella::Utils.image?(@log.first.request_body)
151
+ @request_body = @log.first.request_body
152
+ end
153
+
154
+ @request_body_digest = @log.first.request_body.digest
155
+ @is_binary = Stella::Utils.binary?(@log.first.response_body)
156
+ @is_image = Stella::Utils.image?(@log.first.response_body)
157
+ unless binary? || image?
158
+ @response_body = @log.first.response_body.to_s
159
+ if @response_body.size >= 250_000
160
+ @response_body = @response_body.slice 0, 249_999
161
+ @response_body << ' [truncated]'
162
+ end
163
+ @response_body.force_encoding("UTF-8") if RUBY_VERSION >= "1.9.0"
164
+ begin
165
+ if defined?(Pismo) && @response_body
166
+ doc = Pismo::Document.new @response_body
167
+ @keywords = doc.keywords rescue nil # BUG: undefined method `downcase' for nil:NilClass
168
+ @title = doc.title
169
+ @favicon = doc.favicon
170
+ @author = doc.author
171
+ @lede = doc.lede
172
+ @description = doc.description
173
+ end
174
+ rescue => ex
175
+ Stella.li ex.message
176
+ # /Library/Ruby/Gems/1.8/gems/nokogiri-1.4.1/lib/nokogiri/xml/fragment_handler.rb:37: [BUG] Segmentation fault
177
+ # ruby 1.8.7 (2008-08-11 patchlevel 72) [universal-darwin10.0]
178
+ end
179
+ end
180
+ @response_body_digest = @log.first.response_body.digest
181
+ @log.each { |entry| entry.response_body = ''; entry.request_body = '' }
182
+ processed!
183
+ end
184
+ module ReportMethods
185
+ def log
186
+ content.log
187
+ end
188
+ end
189
+ register :content
190
+ end
191
+
192
+ class Statuses < Storable
193
+ include Gibbler::Complex
194
+ include Report::Plugin
195
+ field :values => Array
196
+ def process(filter={})
197
+ @values = report.content.log.collect { |entry| entry.tag_values(:status) }.flatten
198
+ processed!
199
+ end
200
+ def nonsuccessful
201
+ @values.select { |status| status.to_i >= 400 }
202
+ end
203
+ def successful
204
+ @values.select { |status| status.to_i < 400 }
205
+ end
206
+ def redirected
207
+ @values.select { |status| (300..399).member?(status.to_i) }
208
+ end
209
+ def success?
210
+ nonsuccessful.empty?
211
+ end
212
+ def redirect?
213
+ @values.size == redirected.size
214
+ end
215
+ module ReportMethods
216
+ def redirect?
217
+ statuses.redirect?
218
+ end
219
+ def success?
220
+ statuses.success?
221
+ end
222
+ def statuses_pretty
223
+ pretty = ["Statuses"]
224
+ if statuses.successful.size > 0
225
+ pretty << '%20s: %s' % ['successful', statuses.successful.join(', ')]
226
+ end
227
+ if statuses.nonsuccessful.size > 0
228
+ pretty << '%20s: %s' % ['nonsuccessful', statuses.nonsuccessful.join(', ')]
229
+ end
230
+ pretty.join $/
231
+ end
232
+ end
233
+ register :statuses
234
+ end
235
+
236
+ class Headers < Storable
237
+ include Gibbler::Complex
238
+ include Report::Plugin
239
+ field :request_headers
240
+ field :response_headers
241
+ field :request_headers_digest
242
+ field :response_headers_digest
243
+ def init *args
244
+ # TODO: This doesn't seem to be called anymore.
245
+ @request_headers_hash = {}
246
+ @response_headers_hash = {}
247
+ end
248
+ def process(filter={})
249
+ return if report.content.log.empty?
250
+ @request_headers = report.content.log.first.request_headers
251
+ @response_headers = report.content.log.first.response_headers
252
+ @request_headers_digest = report.content.log.first.request_headers.digest
253
+ @response_headers_digest = report.content.log.first.response_headers.digest
254
+ processed!
255
+ end
256
+ def request_header name=nil
257
+ @request_headers_hash ||= {}
258
+ if @request_headers_hash.empty? && @request_headers
259
+ @request_headers_hash = parse(@request_headers)
260
+ end
261
+ name.nil? ? @request_headers_hash : @request_headers_hash[name.to_s.upcase]
262
+ end
263
+ def response_header name=nil
264
+ @response_headers_hash ||= {}
265
+ if @response_headers_hash.empty? && @response_headers
266
+ @response_headers_hash = parse(@response_headers)
267
+ end
268
+ name.nil? ? @response_headers_hash : @response_headers_hash[name.to_s.upcase]
269
+ end
270
+ def empty?
271
+ @response_headers.to_s.empty?
272
+ end
273
+ private
274
+ def parse str
275
+ headers = {}
276
+ str.split(CRLF).each do|line|
277
+ key, value = line.split(/\s*:\s*/, 2)
278
+ headers[key.upcase] = value
279
+ end
280
+ headers
281
+ end
282
+ register :headers
283
+ end
284
+
285
+ class Metrics < Storable
286
+ include Gibbler::Complex
287
+ include Report::Plugin
288
+ field :response_time => Benelux::Stats::Calculator
289
+ field :socket_connect => Benelux::Stats::Calculator
290
+ field :first_byte => Benelux::Stats::Calculator
291
+ field :last_byte => Benelux::Stats::Calculator
292
+ field :send_request => Benelux::Stats::Calculator
293
+ field :request_headers_size => Benelux::Stats::Calculator
294
+ field :request_content_size => Benelux::Stats::Calculator
295
+ field :response_headers_size => Benelux::Stats::Calculator
296
+ field :response_content_size => Benelux::Stats::Calculator
297
+ field :requests => Integer
298
+ def process(filter={})
299
+ return if processed?
300
+ @response_time = report.timeline.stats.group(:response_time).merge
301
+ @socket_connect = report.timeline.stats.group(:socket_connect).merge
302
+ @first_byte = report.timeline.stats.group(:first_byte).merge
303
+ @send_request = report.timeline.stats.group(:send_request).merge
304
+ @last_byte = report.timeline.stats.group(:last_byte).merge
305
+ #@response_time2 = Benelux::Stats::Calculator.new
306
+ #@response_time2.sample @socket_connect.mean + @send_request.mean + @first_byte.mean + @last_byte.mean
307
+ @requests = report.timeline.stats.group(:requests).merge.n
308
+ @request_headers_size = Benelux::Stats::Calculator.new
309
+ @request_content_size = Benelux::Stats::Calculator.new
310
+ @response_headers_size = Benelux::Stats::Calculator.new
311
+ @response_content_size = Benelux::Stats::Calculator.new
312
+
313
+ @request_content_size.sample report.content.request_body.size unless report.content.request_body.to_s.empty?
314
+ @response_content_size.sample report.content.response_body.size unless report.content.response_body.to_s.empty?
315
+
316
+ @request_headers_size.sample report.headers.request_headers.size unless report.headers.request_headers.to_s.empty?
317
+ @response_headers_size.sample report.headers.response_headers.size unless report.headers.response_headers.to_s.empty?
318
+
319
+ # unless report.content.log.empty?
320
+ # report.content.log.each do |entry|
321
+ # @request_headers_size.sample entry.request_headers.size if entry.request_headers
322
+ # @request_content_size.sample entry.request_body.size if entry.request_body
323
+ # @response_headers_size.sample entry.response_headers.size if entry.response_headers
324
+ # @response_content_size.sample entry.response_body.size if entry.response_body
325
+ # end
326
+ # end
327
+ processed!
328
+ end
329
+ def postprocess
330
+ self.class.field_names.each do |fname|
331
+ next unless self.class.field_types[fname] == Benelux::Stats::Calculator
332
+ hash = send(fname)
333
+ val = Benelux::Stats::Calculator.from_hash hash
334
+ send("#{fname}=", val)
335
+ end
336
+ end
337
+ module ReportMethods
338
+ def metrics_pack
339
+ return unless metrics
340
+ pack = ::MetricsPack.new
341
+ pack.update Stella.now, runid.shorten, metrics.requests, metrics.response_time, metrics.socket_connect, metrics.send_request.to_f.to_s,
342
+ metrics.first_byte, metrics.last_byte, metrics.request_headers_size, metrics.request_content_size,
343
+ metrics.response_headers_size, metrics.response_content_size, 0, error_count
344
+ pack
345
+ end
346
+ def metrics_pretty
347
+ return unless metrics
348
+ pretty = ['Metrics (across %d requests)' % metrics.requests]
349
+ [:socket_connect, :send_request, :first_byte, :last_byte, :response_time].each do |fname|
350
+ val = metrics.send(fname)
351
+ pretty << ('%20s: %8sms' % [fname.to_s.tr('_', ' '), val.mean.to_ms])
352
+ end
353
+ pretty << ''
354
+ [:request_headers_size, :response_content_size].each do |fname|
355
+ val = metrics.send(fname)
356
+ pretty << ('%20s: %8s' % [fname.to_s.tr('_', ' '), val.mean.to_bytes])
357
+ end
358
+ pretty.join $/
359
+ end
360
+ end
361
+ register :metrics
362
+ end
363
+
364
+ field :processed => Boolean
365
+
366
+ attr_reader :runid, :timeline, :filter
367
+ def initialize(timeline=nil, runid=nil)
368
+ @timeline, @runid = timeline, runid
369
+ @processed = false
370
+ end
371
+ def postprocess
372
+ self.class.plugins.each_pair do |name,klass|
373
+ val = klass.from_hash(self.send(name))
374
+ self.send("#{name}=", val)
375
+ end
376
+ end
377
+ def process
378
+ self.class.plugin_order.each do |name|
379
+ klass = self.class.plugins[name]
380
+ Stella.ld "processing #{name}"
381
+ plugin = klass.new self
382
+ plugin.process(filter)
383
+ self.send("#{name}=", plugin)
384
+ end
385
+ @processed = true
386
+ end
387
+ def processed?
388
+ @processed == true
389
+ end
390
+ end
391
+ end
@@ -1,344 +1,470 @@
1
- autoload :CSV, 'csv'
2
- #Gibbler.enable_debug
3
1
 
4
- module Stella
5
- class Testplan < Storable
6
- include Gibbler::Complex
7
- extend Attic
8
-
9
- @file_cache = {}
10
- @globals = {}
11
- class << self
12
- attr_reader :file_cache
13
- attr_reader :globals
14
- def readlines path
15
- if @file_cache.has_key?(path)
16
- Stella.ld "FILE CACHE HIT: #{path}"
17
- return @file_cache[path]
18
- end
19
- Stella.ld "FILE CACHE LOAD: #{path}"
20
- @file_cache[path] = File.readlines(path)
21
- end
22
- def global(n,v=nil)
23
- @globals[n.to_sym] = v unless v.nil?
24
- @globals[n.to_sym]
25
- end
26
- def global?(n)
27
- @globals.has_key?(n.to_sym)
2
+
3
+ class Stella
4
+ module Common
5
+ module PrivacyMethods
6
+ def private?
7
+ privacy == true
8
+ end
9
+ def public?
10
+ !private?
11
+ end
12
+ def private!
13
+ @privacy = true
14
+ end
15
+ def public!
16
+ @privacy = false
17
+ end
28
18
  end
29
19
  end
30
-
31
- attic :base_path
32
- attic :plan_path
33
- attic :description
34
-
35
- field :id => String, &gibbler_id_processor
36
- field :userid => String
37
- field :usecases => Array
38
- field :description => String
39
- #field :resources
40
-
41
- gibbler :usecases, :userid
42
-
43
- def initialize(*uris)
44
- uris.flatten!
45
- self.description = "Test plan"
46
- @usecases = []
47
- @testplan_current_ratio = 0
48
- #@resources = {}
49
-
50
- unless uris.empty?
51
- uris = [uris] unless Array === uris
52
- usecase = Stella::Testplan::Usecase.new
53
- usecase.ratio = 1.0
54
- uris.each do |uri|
55
- req = usecase.add_request :get, uri
20
+ class Testplan < Storable
21
+ include Gibbler::Complex
22
+ include Familia
23
+ include Common::PrivacyMethods
24
+ prefix :testplan
25
+ index :id
26
+ field :id, :class => Gibbler::Digest, :meth => :gibbler, &gibbler_id_processor
27
+ field :custid => String
28
+ field :usecases => Array
29
+ field :desc => String
30
+ field :privacy => Boolean
31
+ field :favicon => String
32
+ field :last_run => Integer
33
+ field :planid => Gibbler::Digest
34
+ field :notify => Boolean
35
+ include Familia::Stamps
36
+ sensitive_fields :custid, :privacy, :notify
37
+ # Don't include privacy in the gibbler calculation because it
38
+ # doesn't make sense to have both a public and private testplan.
39
+ gibbler :custid, :usecases
40
+ def init(uri=nil)
41
+ preprocess
42
+ if uri
43
+ req = Stella::RequestTemplate.new :get, Stella.canonical_uri(uri)
44
+ @usecases << Stella::Usecase.new(req)
56
45
  end
57
- self.add_usecase usecase
58
46
  end
59
- end
60
-
61
- def postprocess
62
- @id &&= Gibbler::Digest.new @id
63
- end
64
-
65
- def self.load_file(path)
66
- conf = File.read path
67
- plan = Stella::Testplan.new
68
- plan.base_path = File.dirname path
69
- plan.plan_path = path
70
- # eval so the DSL code can be executed in this namespace.
71
- plan.instance_eval conf, path
72
- plan
73
- end
74
-
75
- def check!
76
- # Adjust ratios if necessary
77
- needy = @usecases.select { |u| u.ratio == -1 }
78
- needy.each do |u|
79
- u.ratio = (remaining_ratio / needy.size).to_f
47
+ alias_method :planid, :id
48
+ def favicon?() !@favicon.nil? && !@favicon.empty? end
49
+ def preprocess
50
+ @usecases ||= []
51
+ @privacy = false if @privacy.nil?
80
52
  end
81
- if @testplan_current_ratio > 1.0
82
- msg = "Usecase ratio cannot be higher than 1.0"
83
- msg << " (#{@testplan_current_ratio})"
84
- raise Stella::WackyRatio, msg
53
+ def first_request
54
+ return if @usecases.empty?
55
+ @usecases.first.requests.first
56
+ end
57
+ def freeze
58
+ return if frozen?
59
+ @usecases.each { |uc| uc.freeze }
60
+ @id &&= Gibbler::Digest.new(@id || self.digest)
61
+ super
62
+ self
63
+ end
64
+ def postprocess
65
+ @id &&= Gibbler::Digest.new(@id)
66
+ @notify ||= false
67
+ @notify &&= false if @notify == 'false'
68
+ end
69
+ def cust
70
+ @cust ||= Customer.from_redis @custid
71
+ @cust || Customer.anonymous
72
+ end
73
+ def monitor
74
+ MonitorInfo.from_redis @id
75
+ end
76
+ def monitored?
77
+ mon = monitor
78
+ mon && mon.enabled
79
+ end
80
+ def host
81
+ h = @host.nil? || frozen? ? HostInfo.load_or_create(hostid) : @host
82
+ frozen? ? h : (@host=h)
83
+ end
84
+ def hostid
85
+ h = (@hostid.nil? || frozen?) && first_request ? Stella.canonical_host(first_request.uri) : @hostid
86
+ frozen? ? h : (@hostid=h)
87
+ end
88
+ def destroy!
89
+ raise BS::Problem, "Monitor exists #{index}" if MonitorInfo.exists?(index)
90
+ host.testplans.rem self unless host.nil?
91
+ cust.testplans.rem self unless cust.nil?
92
+ super
93
+ end
94
+ def owner?(guess)
95
+ custid != nil && cust.custid?(guess)
96
+ end
97
+ def checkup base_uri, opts={}
98
+ opts[:base_uri] = base_uri
99
+ run Stella::Engine::Checkup, opts
100
+ end
101
+ def run engine, opts={}
102
+ testrun = Stella::Testrun.new self, engine.mode, opts
103
+ engine.run testrun
104
+ end
105
+ module ClassMethods
106
+ def usecases
107
+ @usecases ||= []
108
+ @usecases
109
+ end
110
+ def checkup base_uri, opts={}
111
+ Stella::Testplan.plan(self).checkup base_uri, opts
112
+ end
113
+ def run engine, opts={}
114
+ Stella::Testplan.plan(self).run engine, opts
115
+ end
116
+ def testplan
117
+ Stella::Testplan.plan(self)
118
+ end
119
+ # Session objects will extend registered classes.
120
+ def register klass=nil
121
+ unless klass.nil?
122
+ @registered_classes ||= []
123
+ @registered_classes << klass
124
+ end
125
+ @registered_classes
126
+ end
127
+ attr_reader :registered_classes
128
+ def session
129
+ @session ||= {}
130
+ @session
131
+ end
132
+ end
133
+ class << self
134
+ attr_reader :plans
135
+ def inherited obj
136
+ super
137
+ obj.extend ClassMethods
138
+ end
139
+ def from_hash(*args)
140
+ me = super(*args)
141
+ me.usecases.collect! { |uc| Stella::Usecase.from_hash(uc) }
142
+ me
143
+ end
144
+ def plans
145
+ @plans ||= {}
146
+ @plans
147
+ end
148
+ def plan(klass,v=nil)
149
+ # Store the class as a string. Ruby calls Object#hash before setting
150
+ # the hash key which conflicts with Familia::Object.hash.
151
+ plans[klass.to_s] = v unless v.nil?
152
+ plans[klass.to_s]
153
+ rescue NameError => ex
154
+ nil
155
+ end
156
+ def plan?(name)
157
+ !plan(name).nil?
158
+ end
159
+ def global?(name)
160
+ global.has_key?(name)
161
+ end
162
+ def global
163
+ @global ||= {}
164
+ @global
165
+ end
85
166
  end
86
167
  end
87
-
88
- # make sure all clients share identical test plans
89
- def freeze
90
- return if frozen?
91
- Stella.ld "FREEZE TESTPLAN: #{self.description}"
92
- @usecases.each { |uc| uc.freeze }
93
- super
94
- self
95
- end
96
-
97
- def id
98
- Gibbler::Digest.new(@id || self.digest)
99
- end
100
-
101
- def self.from_hash(*args)
102
- me = super(*args)
103
- me.usecases.collect! { |uc| Stella::Testplan::Usecase.from_hash(uc) }
104
- me
105
- end
106
-
107
- def usecase(*args, &blk)
108
- return @usecases if args.empty? && blk.nil?
109
- ratio, name = nil,nil
110
- unless args.empty?
111
- ratio, name = args[0], args[1] if args[0].is_a?(Numeric)
112
- ratio, name = args[1], args[0] if args[0].is_a?(String)
113
- end
114
- uc = Stella::Testplan::Usecase.new
115
- uc.base_path = self.base_path
116
- uc.plan_path = self.plan_path
117
- uc.instance_eval &blk
118
- uc.ratio = (ratio || -1).to_f
119
- uc.description = name unless name.nil?
120
- @testplan_current_ratio += uc.ratio if uc.ratio > 0
121
- add_usecase uc
168
+ class Usecase < Storable
169
+ include Gibbler::Complex
170
+ field :id, :class => Gibbler::Digest, :meth => :gibbler, &gibbler_id_processor
171
+ field :desc => String
172
+ field :ratio => Float
173
+ field :requests => Array
174
+ field :http_auth => Hash
175
+ field :ucid => Gibbler::Digest
176
+ gibbler :requests
177
+ def initialize(req=nil)
178
+ preprocess
179
+ @requests << req if req
180
+ end
181
+ alias_method :ucid, :id
182
+ def preprocess
183
+ @requests ||= []
184
+ end
185
+ def postprocess
186
+ @id &&= Gibbler::Digest.new(@id)
187
+ end
188
+ def freeze
189
+ return if frozen?
190
+ @requests.each { |r| r.freeze }
191
+ @id &&= Gibbler::Digest.new(@id || self.digest)
192
+ super
193
+ self
194
+ end
195
+ module ClassMethods
196
+ [:get, :put, :head, :post, :delete].each do |meth|
197
+ define_method meth do |*args,&definition|
198
+ path, opts = *args
199
+ create_request_template meth, path, opts, &definition
200
+ end
201
+ end
202
+ [:xget, :xput, :xhead, :xpost, :xdelete].each do |ignore|
203
+ define_method ignore do |*args|
204
+ Stella.ld " ignoring #{ignore}: #{args.inspect}"
205
+ end
206
+ end
207
+ def http_auth user, pass=nil, domain=nil
208
+ planname, ucname = *names
209
+ uc = Stella::Testplan.plan(planname).usecases.last
210
+ uc.http_auth = { :user => user, :pass => pass, :domain => domain }
211
+ uc.http_auth
212
+ end
213
+ # Session objects will extend registered classes.
214
+ def register klass=nil
215
+ unless klass.nil?
216
+ @registered_classes ||= []
217
+ @registered_classes << klass
218
+ end
219
+ @registered_classes
220
+ end
221
+ attr_reader :registered_classes
222
+ def session
223
+ @session ||= {}
224
+ @session
225
+ end
226
+ private
227
+ def create_request_template meth, path, opts=nil, &definition
228
+ opts ||= {}
229
+ planname, ucname = *names
230
+ uc = Stella::Testplan.plan(planname).usecases.last
231
+ Stella.ld " (#{uc.class}) define: #{meth} #{path} #{opts if !opts.empty?}"
232
+ rt = RequestTemplate.new meth, path, opts, &definition
233
+ uc.requests << rt
234
+ end
235
+ end
236
+ class << self
237
+ attr_accessor :instance, :testplan
238
+ # The class syntax uses the session method defined in ClassMethods.
239
+ # This is here for autogenerated usecases and ones loaded from JSON.
240
+ attr_reader :registered_classes, :session
241
+ def from_hash(*args)
242
+ me = super(*args)
243
+ me.requests.collect! { |req| Stella::RequestTemplate.from_hash(req) }
244
+ me
245
+ end
246
+ def checkup base_uri, opts={}
247
+ (opts[:usecases] ||= []) << self
248
+ testplan.checkup base_uri, opts
249
+ end
250
+ def names
251
+ names = self.to_s.split('::')
252
+ planname, ucname = case names.size
253
+ when 1 then ['DefaultTestplan', names.last]
254
+ else [names[0..-2].join('::'), names[-1]] end
255
+ [eval(planname), ucname.to_sym]
256
+ end
257
+ def inherited(obj)
258
+ super
259
+ planclass, ucname = *obj.names
260
+ planclass.extend Stella::Testplan::ClassMethods
261
+ unless Stella::Testplan.plan? planclass
262
+ Stella::Testplan.plan(planclass, planclass.new)
263
+ Stella::Testplan.plan(planclass).desc = planclass
264
+ end
265
+
266
+ obj.instance = obj.new
267
+ obj.testplan = Stella::Testplan.plan(planclass)
268
+ Stella::Testplan.plan(planclass).usecases << obj.instance
269
+ Stella::Testplan.plan(planclass).usecases.last.desc = ucname
270
+ obj.extend ClassMethods
271
+ end
272
+ end
122
273
  end
123
- def xusecase(*args, &blk); Stella.ld "Skipping usecase"; end
124
-
125
- def add_usecase(uc)
126
- Stella.ld "Usecase: #{uc.description}"
127
- @usecases << uc
128
- uc
274
+
275
+ class EventTemplate < Storable
276
+ include Gibbler::Complex
129
277
  end
130
278
 
131
- # for DSL use-only (otherwise use: self.description)
132
- def desc(*args)
133
- self.description = args.first unless args.empty?
134
- self.description
279
+ class StringTemplate
280
+ include Gibbler::String
281
+ attr_reader :src
282
+ def initialize(src)
283
+ src = src.to_s
284
+ @src, @template = src, ERB.new(src)
285
+ end
286
+ def result(binding)
287
+ @template.result(binding)
288
+ end
289
+ def to_s() src end
135
290
  end
136
291
 
137
- def pretty(long=false)
138
- self.digest # make sure we have values in the cache
139
- str = []
140
- dig = long ? self.digest_cache : self.digest_cache.shorter
141
- str << " %-66s ".att(:reverse) % ["#{self.description} (#{dig})"]
142
- @usecases.each_with_index do |uc,i|
143
- uc.digest
144
- dig = long ? uc.digest_cache : uc.digest_cache.shorter
145
- desc = uc.description || "Usecase ##{i+1}"
146
- desc += " (#{dig}) "
147
- str << (' ' << " %-61s %s%% ".att(:reverse).bright) % [desc, uc.ratio_pretty]
148
- unless uc.http_auth.nil?
149
- str << ' Auth: %s (%s/%s)' % uc.http_auth.values
150
- end
151
- requests = uc.requests.each do |r|
152
- r.digest
153
- dig = long ? r.digest_cache : r.digest_cache.shorter
154
- str << " %-62s".bright % ["#{r.description} (#{dig})"]
155
- str << " %s" % [r]
156
- if Stella.stdout.lev > 1
157
- [:wait].each { |i| str << " %s: %s" % [i, r.send(i)] }
158
- str << ' %s: %s' % ['params', r.params.inspect]
159
- r.response_handler.each do |status,proc|
160
- str << " response: %s%s" % [status, proc.source.split($/).join("#{$/} ")]
161
- end
292
+ class RequestTemplate < EventTemplate
293
+ field :id, :class => Gibbler::Digest, :meth => :gibbler, &gibbler_id_processor
294
+ field :protocol => Symbol
295
+ field :http_method
296
+ field :http_version
297
+ field :http_auth
298
+ field :uri => String do |v| v.to_s end
299
+ field :params => Hash
300
+ field :headers => Hash
301
+ field :body
302
+ field :desc
303
+ field :rtid => Gibbler::Digest
304
+ field :wait => Range
305
+ field :response_handler => Hash, &hash_proc_processor
306
+ field :follow => Boolean
307
+ attr_accessor :callback
308
+ gibbler :http_method, :uri, :http_version, :params, :headers, :body
309
+ def initialize(meth=nil, uri=nil, opts={}, &definition)
310
+ @protocol = :http
311
+ @http_method, @uri = meth, uri
312
+ opts.each_pair { |n,v| self.send("#{n}=", v) if self.class.has_field?(n) }
313
+ @params ||= {}
314
+ @headers ||= {}
315
+ @follow ||= false
316
+ @callback = definition
317
+ end
318
+ def postprocess
319
+ @id &&= Gibbler::Digest.new(@id)
320
+ unless response_handler.nil?
321
+ response_handler.keys.each do |range|
322
+ proc = response_handler[range]
323
+ response_handler[range] = Proc.from_string(proc) if proc.kind_of?(ProcString)
162
324
  end
163
325
  end
164
326
  end
165
- str.join($/)
166
- end
167
-
168
- private
169
- def remaining_ratio
170
- 1.0 - @testplan_current_ratio
327
+ def freeze
328
+ return if frozen?
329
+ @id &&= Gibbler::Digest.new(@id || self.digest)
330
+ super
331
+ self
332
+ end
333
+ alias_method :param, :params
334
+ alias_method :header, :headers
171
335
  end
336
+
337
+ TP = Testplan
338
+ UC = Usecase
339
+ RT = RequestTemplate
340
+ ET = EventTemplate
172
341
 
173
342
  end
174
- end
175
-
176
-
177
- module Stella
178
- class Testplan
179
343
 
180
- #
181
- # Any valid Ruby syntax will do the trick:
182
- #
183
- # usecase(10, "Self-serve") {
184
- # post("/listing/add", "Add a listing") {
185
- # wait 1..4
186
- # param :name => random(8)
187
- # param :city => "Vancouver"
188
- # response(302) {
189
- # repeat 3
190
- # }
191
- # }
192
- # }
193
- #
194
- class Usecase < Storable
344
+
345
+
346
+ class Stella
347
+ class Testrun < Storable
195
348
  include Gibbler::Complex
196
- include Stella::Data::Helpers
197
- extend Attic
198
-
199
- class Auth < Struct.new(:domain, :user, :pass)
200
- include Gibbler::Complex
349
+ include Familia
350
+ prefix :testrun
351
+ index :id
352
+ include Familia::Stamps
353
+ include Common::PrivacyMethods
354
+ field :id, :class => Gibbler::Digest, :meth => :gibbler, &gibbler_id_processor
355
+ field :custid => String
356
+ field :status => Symbol
357
+ field :options => Hash
358
+ field :mode => Symbol
359
+ field :hosts
360
+ field :ctime => Float
361
+ field :stime => Float
362
+ field :etime => Float
363
+ field :salt
364
+ field :planid => Gibbler::Digest
365
+ field :runid => Gibbler::Digest
366
+ field :hostid
367
+ field :privacy => Boolean
368
+ field :report => Stella::Report
369
+ sensitive_fields :custid, :salt, :privacy
370
+ gibbler :salt, :planid, :custid, :hosts, :mode, :options, :ctime
371
+ attr_reader :plan
372
+ alias_method :start_time, :stime
373
+ alias_method :end_time, :etime
374
+ def init plan=nil, mode=nil, options={}
375
+ @ctime = Stella.now
376
+ @plan, @mode = plan, mode
377
+ @options = {
378
+ }.merge options
379
+ preprocess
201
380
  end
202
-
203
- attic :base_path # we don't want gibbler to see this
204
- attic :plan_path
205
-
206
- field :id => String, &gibbler_id_processor
207
-
208
- attic :description
209
- field :description => String
210
-
211
- field :ratio => Float
212
- field :http_auth => Hash
213
- field :timeout => Float
214
- field :requests => Array
215
- field :resources => Hash
216
-
217
- class UnknownResource < Stella::Error
218
- def message; "UnknownResource: #{@obj}"; end
381
+ alias_method :runid, :id
382
+ def duration
383
+ return 0 unless @stime
384
+ (@etime || Stella.now) - @stime
219
385
  end
220
-
221
- def initialize(&blk)
222
- @requests, @resources = [], {}
223
- instance_eval &blk unless blk.nil?
386
+ def errors?
387
+ @report && @report.errors?
224
388
  end
225
-
226
- def id
227
- Gibbler::Digest.new(@id || self.digest)
389
+ def preprocess
390
+ @salt ||= Stella.now.digest.short
391
+ @status ||= :new
392
+ if @plan
393
+ @planid = @plan.id
394
+ @hostid = @plan.hostid
395
+ end
396
+ @options ||= {}
397
+ @privacy ||= false
228
398
  end
229
-
230
- def self.from_hash(hash={})
231
- me = super(hash)
232
- me.requests.collect! { |req| Stella::Data::HTTP::Request.from_hash(req) }
233
- me
399
+ def postprocess
400
+ @id &&= Gibbler::Digest.new(@id)
401
+ # Calling plan calls Redis.
402
+ #@privacy = plan.privacy if Stella::Testplan === plan
403
+ @report = Stella::Report.from_hash @report if Hash === @report
404
+ @planid &&= Gibbler::Digest.new(@planid)
234
405
  end
235
-
236
-
237
- def desc(*args)
238
- self.description = args.first unless args.empty?
239
- self.description
406
+ def hostid
407
+ # NOTE: This method is needed only until May 30 or so.
408
+ # (there was an issue where incidents were not including a hostid)
409
+ @hostid || (plan.nil? ? nil : plan.hostid)
240
410
  end
241
-
242
- def timeout(*args)
243
- @timeout = args.first unless args.empty?
244
- @timeout
411
+ def run opts={}
412
+ raise StellaError.new("No mode") unless Stella::Engine.mode?(@mode)
413
+ engine = Stella::Engine.load(@mode)
414
+ opts.merge! @options
415
+ engine.run self, opts
416
+ save if respond_to? :save
417
+ self.report
245
418
  end
246
-
247
- def resource(name, *args)
248
- if Hash === name
249
- Stella.ld "ARGS IGNORED: #{args.inspect} (#{caller[0]})" if !args.empty?
250
- @resources.merge! name
251
- elsif !name.nil? && !args.empty?
252
- @resources.merge!({name => args[0]})
253
- elsif @resources.has_key?(name)
254
- @resources[name]
255
- elsif Stella::Testplan.global?(name)
256
- Stella::Testplan.global(name)
257
- end
258
- end
259
- alias_method :set, :resource
260
-
261
- def ratio
262
- r = (@ratio || 0).to_f
263
- r = r/100 if r > 1
264
- r
419
+ def checkup?
420
+ @mode == :checkup
265
421
  end
266
-
267
- def ratio_pretty
268
- r = (@ratio || 0).to_f
269
- r > 1.0 ? r.to_i : (r * 100).to_i
422
+ def monitor?
423
+ @mode == :monitor
270
424
  end
271
-
272
- # Reads the contents of the file <tt>path</tt> (the current working
273
- # directory is assumed to be the same directory containing the test plan).
274
- def read(path)
275
- path = File.join(base_path, path) if base_path
276
- Stella.ld "READING FILE: #{path}"
277
- Stella::Testplan.readlines path
425
+ def destroy!
426
+ VendorInfo.global.checkups.remove self
427
+ host.checkups.remove self if host
428
+ plan.checkups.remove self if plan
429
+ cust.checkups.remove self if cust
430
+ super
278
431
  end
279
-
280
- def list(path)
281
- read(path).collect { |line| line.strip }
432
+ def plan
433
+ if @plan.nil?
434
+ @plan = Stella::Testplan.from_redis @planid
435
+ @plan.freeze
436
+ end
437
+ @plan
282
438
  end
283
-
284
- def csv(path)
285
- path = File.join(base_path, path) if base_path
286
- Stella.ld "READING CSV: #{path}"
287
- file = Stella::Testplan.readlines path
288
- CSV.parse file.join
439
+ def owner?(obj)
440
+ obj = Customer === obj ? obj.custid : obj
441
+ cust.username?(obj)
289
442
  end
290
-
291
- def quickcsv(path)
292
- path = File.join(base_path, path) if base_path
293
- Stella.ld "READING CSV(QUICK): #{path}"
294
- ar = []
295
- file = Stella::Testplan.readlines path
296
- file.each do |l|
297
- l.strip!
298
- ar << l.split(',')
299
- end
300
- ar
443
+ def cust
444
+ @cust ||= Customer.from_redis @custid
445
+ @cust || Customer.anonymous
301
446
  end
302
-
303
- def freeze
304
- return if frozen?
305
- @requests.each { |r| r.freeze }
306
- self.id ||= self.digest
307
- super
308
- self
447
+ def host
448
+ @host ||= plan.host if plan
449
+ @host
309
450
  end
310
-
311
- def auth(user, pass=nil, domain=nil)
312
- @http_auth ||= Auth.new
313
- @http_auth.user, @http_auth.pass, @http_auth.domain = user, pass, domain
451
+ class << self
452
+ attr_reader :statuses
453
+ end
454
+ @statuses = [:new, :pending, :running, :done, :failed, :fubar, :cancelled]
455
+ @statuses.each do |status|
456
+ define_method :"#{status}?" do
457
+ @status == status
458
+ end
459
+ define_method :"#{status}!" do
460
+ @status = status
461
+ begin
462
+ save if respond_to? :save
463
+ rescue Errno::ECONNREFUSED => ex
464
+ Stella.ld ex.message
465
+ end
466
+ end
314
467
  end
315
-
316
- def add_request(meth, *args, &blk)
317
- if args[0].nil?
318
- msg = "No URI given for request ##{@requests.size} "
319
- msg << "#{meth} block in #{self.plan_path}"
320
- raise Stella::Error, msg
321
- end
322
- req = Stella::Data::HTTP::Request.new meth.to_s.upcase, args[0], &blk
323
- req.description = args[1] if args.size > 1 # Description is optional
324
- Stella.ld req
325
- @requests << req
326
- req
327
- end
328
- def get(*args, &blk); add_request :get, *args, &blk; end
329
- def put(*args, &blk); add_request :put, *args, &blk; end
330
- def post(*args, &blk); add_request :post, *args, &blk; end
331
- def head(*args, &blk); add_request :head, *args, &blk; end
332
- def delete(*args, &blk); add_request :delete, *args, &blk; end
333
-
334
- def xget(*args, &blk); Stella.ld "Skipping get" end
335
- def xput(*args, &blk); Stella.ld "Skipping put" end
336
- def xpost(*args, &blk); Stella.ld "Skipping post" end
337
- def xhead(*args, &blk); Stella.ld "Skipping head" end
338
- def xdelete(*args, &blk); Stella.ld "Skipping delete" end
339
-
340
468
  end
341
-
342
469
  end
343
- end
344
-
470
+ class DefaultTestplan < Stella::Testplan; end