stella 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/README.txt +135 -0
  2. data/Rakefile +100 -0
  3. data/bin/stella +12 -0
  4. data/lib/stella.rb +58 -0
  5. data/lib/stella/adapter/ab.rb +303 -0
  6. data/lib/stella/adapter/base.rb +87 -0
  7. data/lib/stella/adapter/httperf.rb +296 -0
  8. data/lib/stella/adapter/siege.rb +321 -0
  9. data/lib/stella/cli.rb +291 -0
  10. data/lib/stella/cli/agents.rb +73 -0
  11. data/lib/stella/cli/base.rb +19 -0
  12. data/lib/stella/cli/language.rb +18 -0
  13. data/lib/stella/cli/localtest.rb +80 -0
  14. data/lib/stella/command/base.rb +111 -0
  15. data/lib/stella/command/localtest.rb +339 -0
  16. data/lib/stella/logger.rb +63 -0
  17. data/lib/stella/response.rb +82 -0
  18. data/lib/stella/storable.rb +116 -0
  19. data/lib/stella/support.rb +106 -0
  20. data/lib/stella/test/base.rb +34 -0
  21. data/lib/stella/test/definition.rb +79 -0
  22. data/lib/stella/test/run/summary.rb +50 -0
  23. data/lib/stella/test/summary.rb +82 -0
  24. data/lib/stella/text.rb +64 -0
  25. data/lib/stella/text/resource.rb +39 -0
  26. data/lib/utils/crypto-key.rb +84 -0
  27. data/lib/utils/escape.rb +302 -0
  28. data/lib/utils/fileutil.rb +59 -0
  29. data/lib/utils/httputil.rb +210 -0
  30. data/lib/utils/mathutil.rb +78 -0
  31. data/lib/utils/textgraph.rb +267 -0
  32. data/lib/utils/timerutil.rb +58 -0
  33. data/support/text/en.yaml +54 -0
  34. data/support/text/nl.yaml +1 -0
  35. data/support/useragents.txt +75 -0
  36. data/vendor/useragent/MIT-LICENSE +20 -0
  37. data/vendor/useragent/README +21 -0
  38. data/vendor/useragent/init.rb +1 -0
  39. data/vendor/useragent/lib/user_agent.rb +83 -0
  40. data/vendor/useragent/lib/user_agent/browsers.rb +24 -0
  41. data/vendor/useragent/lib/user_agent/browsers/all.rb +69 -0
  42. data/vendor/useragent/lib/user_agent/browsers/gecko.rb +43 -0
  43. data/vendor/useragent/lib/user_agent/browsers/internet_explorer.rb +40 -0
  44. data/vendor/useragent/lib/user_agent/browsers/opera.rb +49 -0
  45. data/vendor/useragent/lib/user_agent/browsers/webkit.rb +94 -0
  46. data/vendor/useragent/lib/user_agent/comparable.rb +25 -0
  47. data/vendor/useragent/lib/user_agent/operating_systems.rb +19 -0
  48. data/vendor/useragent/spec/browsers/gecko_user_agent_spec.rb +209 -0
  49. data/vendor/useragent/spec/browsers/internet_explorer_user_agent_spec.rb +99 -0
  50. data/vendor/useragent/spec/browsers/opera_user_agent_spec.rb +59 -0
  51. data/vendor/useragent/spec/browsers/other_user_agent_spec.rb +19 -0
  52. data/vendor/useragent/spec/browsers/webkit_user_agent_spec.rb +373 -0
  53. data/vendor/useragent/spec/spec_helper.rb +1 -0
  54. data/vendor/useragent/spec/user_agent_spec.rb +331 -0
  55. data/vendor/useragent/useragent.gemspec +12 -0
  56. metadata +139 -0
@@ -0,0 +1,111 @@
1
+
2
+
3
+ module Stella::Command
4
+ class Base
5
+
6
+ BrowserNicks = {
7
+ 'ff' => 'firefox',
8
+ 'ie' => 'internetexplorer'
9
+ }.freeze unless defined? BrowserNicks
10
+
11
+ OperatingSystemNicks = {
12
+ 'win' => 'windows',
13
+ 'lin' => 'linux',
14
+ 'osx' => 'osx',
15
+ 'freebsd' => 'bsd',
16
+ 'netbsd' => 'bsd',
17
+ 'openbsd' => 'bsd'
18
+ }.freeze unless defined? OperatingSystemNicks
19
+
20
+ # TODO: See EC2::Platform for example to improve/generalize platform
21
+ # discovery. We'll need this for monitoring.
22
+ IMPLEMENTATIONS = [
23
+ [/darwin/i, :unix, :macosx ]
24
+ ]
25
+ ARCHITECTURES = [
26
+ [/(i\d86)/i, :i386 ]
27
+ ]
28
+
29
+ # When using Stella::CLI this will contain the string used to call this command
30
+ # i.e. ab, siege, help, etc...
31
+ attr_accessor :shortname
32
+
33
+
34
+ def initialize()
35
+
36
+ #agent = find_agent(*expand_str(v))
37
+ #@logger.info(:cli_print_agent, agent) if @options.verbose >= 1
38
+
39
+ end
40
+
41
+ def run_sleeper(duration)
42
+ remainder = duration % 1
43
+ duration.to_i.times {
44
+ Stella::LOGGER.info_print('.') unless @quiet
45
+ sleep 1
46
+ }
47
+ sleep remainder if remainder > 0
48
+ end
49
+
50
+ # find_agent
51
+ #
52
+ # Takes an input string which can be either a shortname or a complete
53
+ # user agent string. If the string matches the shortname format, it
54
+ # will select an agent string from useragents.txt based on the shortname.
55
+ # Shortname takes the following format: browser-version-os.
56
+ # Examples: ff-3-linux, ie-5, opera-10-win, chrome-0.2-osx, random
57
+ # If os doesn't match, it will look for the browser and version. If it can't
58
+ # find the version it will look for the browser and apply the version given.
59
+ # If browser doesn't match a known browser, it assumes the string is a
60
+ # complete user agent and simply returns that value.
61
+ def find_agent(name,second=nil,third=nil)
62
+ name = (BrowserNicks.has_key?(name)) ? BrowserNicks[name] : name
63
+ return name unless @available_agents.has_key?(name) || name == "random"
64
+
65
+ index = name
66
+ if (second && third) # i.e. opera-9-osx
67
+ os = (OperatingSystemNicks.has_key?(third)) ? OperatingSystemNicks[third] : third
68
+ index = "#{name}-#{second}-#{os}"
69
+ elsif(second && second.to_i > 0) # i.e. opera-9
70
+ index = "#{name}-#{second}"
71
+ elsif(second) # i.e. opera-osx
72
+ os = (OperatingSystemNicks.has_key?(second)) ? OperatingSystemNicks[second] : second
73
+ index = "#{name}-#{os}"
74
+ elsif(name == "random")
75
+ index = @available_agents.keys[ rand(@available_agents.keys.size) ]
76
+ end
77
+
78
+ # Attempt to find a pool of user agents that match the supplied index
79
+ ua_pool = @available_agents[index]
80
+
81
+ # In the event we don't find an agent above (which will only happen
82
+ # when the user provided a version), we'll take a random agent for
83
+ # the same browser and apply the version supplied by the user. We
84
+ # create the index using just the major version number so if the user
85
+ # supplies a specific verswion number, they will always end up here.
86
+ unless ua_pool
87
+ os = (OperatingSystemNicks.has_key?(third)) ? OperatingSystemNicks[third] : third
88
+ index = (os) ? "#{name}-#{os}" : name
89
+ ua_tmp = @available_agents[index][ rand(@available_agents[index].size) ]
90
+ ua_tmp.version = second if second.to_i > 0
91
+ ua_pool = [ua_tmp]
92
+ end
93
+
94
+ ua = ua_pool[ rand(ua_pool.size) ]
95
+
96
+ ua.to_s
97
+
98
+ end
99
+
100
+
101
+ #
102
+ # Generates a string of random alphanumeric characters
103
+ # These are used as IDs throughout the system
104
+ def strand( len )
105
+ chars = ("a".."z").to_a + ("0".."9").to_a
106
+ newpass = ""
107
+ 1.upto(len) { |i| newpass << chars[rand(chars.size-1)] }
108
+ return newpass
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,339 @@
1
+
2
+
3
+ module Stella
4
+ class LocalTest < Stella::Command::Base
5
+
6
+
7
+
8
+ # A container for the test parameters
9
+ attr_accessor :testdef
10
+ # The load tool adapter
11
+ attr_accessor :adapter
12
+
13
+ attr_accessor :test_path
14
+
15
+ attr_accessor :quiet
16
+ attr_accessor :guid
17
+ attr_accessor :verbose
18
+ attr_accessor :format
19
+
20
+ # list of all filesystem paths for each run in a single test
21
+ attr_reader :test_runpaths
22
+ # same as above, expect for all tests
23
+ attr_reader :all_runpaths
24
+ # list of all hostname:port in the test
25
+ attr_reader :hosts
26
+ # list of all /paths in the test
27
+ attr_reader :paths
28
+
29
+ attr_accessor :working_directory
30
+ attr_reader :available_agents
31
+
32
+ def initialize(testdef=nil, adapter=nil)
33
+ @testdef = testdef if testdef
34
+ @adapter = adapter if adapter
35
+
36
+ @guid = Crypto.sign(rand.to_s, rand.to_s)
37
+
38
+ @test_runpaths = []
39
+ @all_runpaths = []
40
+ @hosts = []
41
+ @paths = []
42
+ @format = 'yaml'
43
+ @agents = []
44
+
45
+ @available_agents = index_available_agents(File.join(STELLA_HOME, 'support', 'useragents.txt'))
46
+ end
47
+
48
+ def index_available_agents(path)
49
+ Stella::LOGGER.debug("LOADING #{path}")
50
+ return [] unless File.exists?(path)
51
+ Stella::Util.process_useragents(FileUtil.read_file_to_array(path))
52
+ end
53
+
54
+ def translate_requested_agents(possible_agents=[])
55
+ agents = []
56
+ possible_agents.each do |a|
57
+ agents << find_agent(*a)
58
+ end
59
+ agents
60
+ end
61
+
62
+ def run
63
+
64
+
65
+ raise UnavailableAdapter.new(@adapter.name) unless @adapter.available?
66
+
67
+ unless @adapter.loadtest?
68
+ system(@adapter.command)
69
+
70
+ return
71
+ end
72
+
73
+ @agents = translate_requested_agents(@testdef.agents)
74
+
75
+ test_path = _generate_test_path
76
+
77
+ prepare_test(test_path)
78
+
79
+ threshold = (@testdef.rampup) ? @testdef.rampup.ceiling : @adapter.vusers
80
+
81
+ runnum = "00"
82
+ while(@adapter.vusers <= threshold) do
83
+
84
+ # Make sure the load factor is set to 1. If there was a warmup,
85
+ # then this value will be less than 1.
86
+ testrun = maker_of_testruns(1)
87
+
88
+ # We empty test_runpaths so we can keep track of the runpaths for each
89
+ # level of virtual users. This is useful during a rampup test so we can
90
+ # collect the stats for the repetitions at each virtual user level.
91
+ @test_runpaths = []
92
+
93
+ # Execute each test run in turn. All of the steps are the same for each
94
+ # run. This is important because the purpose of the test runs is to
95
+ # create a statistical certainty of performance.
96
+ @testdef.repetitions.times do
97
+
98
+ # This value is used to create the run directory and show which run we're on
99
+ runnum.succ!
100
+
101
+ # Generate the filesystem path to store the run data.
102
+ runpath = File.join(test_path, "run#{runnum}")
103
+
104
+ # Keep track of every run path. We use this in postpare to
105
+ # do some stuff after all the tests have run
106
+ @all_runpaths << runpath
107
+
108
+ # test_runpaths will be identical to all_runpaths except when there is a
109
+ # rampup. In that case, test_runpaths will contain the runpaths for all
110
+ # repetitions of each level of virtual users.
111
+ @test_runpaths << runpath
112
+
113
+ testrun.call("Run #{runnum}", runpath)
114
+
115
+ # Only sleep if requested and there is more than 1 test run
116
+ if @testdef.sleep && @testdef.sleep.to_f > 0 && @testdef.repetitions > 1
117
+ run_sleeper(@testdef.sleep)
118
+ end
119
+ Stella::LOGGER.info('') unless @quiet # We want the newline
120
+ end
121
+
122
+ # Rampup tests produce multiple summary files which include the batch
123
+ # number. Regular runs have just one file we set here as the default.
124
+ summary_name = "SUMMARY"
125
+
126
+ # It's possible for the interval to not divide evenly into the ceiling
127
+ # If we have room between the current number of virtual users and the
128
+ # ceiling, we'll add the difference for the final test run.
129
+ if (@testdef.rampup)
130
+ final_total = @adapter.vusers + @testdef.rampup.interval
131
+ if final_total > threshold && (threshold - @adapter.vusers) > 0
132
+ @adapter.vusers += (threshold - @adapter.vusers)
133
+ else
134
+ @adapter.vusers += @testdef.rampup.interval
135
+ end
136
+ padded_users = @adapter.vusers.to_s.rjust(4, '0')
137
+ summary_name = "SUMMARY-#{padded_users}"
138
+ end
139
+
140
+ # Read the run summaries for this batch and produce totals, averages,
141
+ # and standard deviations.
142
+ test_stats = process_test_stats(@test_runpaths)
143
+ print_summary(test_stats) if (@testdef.repetitions > 1)
144
+ save_summary(File.join(test_path, "#{summary_name}.#{@format}"), test_stats)
145
+
146
+
147
+ # Non-rampup tests only need to run through the loop once.
148
+ break if (!@testdef.rampup && @adapter.vusers == threshold)
149
+
150
+ end
151
+
152
+ if @testdef.rampup
153
+ # Notice the difference between these test stats and the ones above?
154
+ # These stats are based on the entire rampup test, across all levels
155
+ # of virtual users.
156
+ test_stats = process_test_stats(@all_runpaths)
157
+ save_summary(File.join(test_path, "SUMMARY-RAMPUP.#{@format}"), test_stats)
158
+ print_summary(test_stats) if (@testdef.repetitions > 1)
159
+ end
160
+ end
161
+
162
+ def latest_test_symlink_path
163
+ return unless @working_directory
164
+ File.join(@working_directory, 'latest')
165
+ end
166
+
167
+ private
168
+
169
+ # prepare_test
170
+ #
171
+ # Execute the group of testruns associated to this test. This includes
172
+ # zero or one warmup and one or more testruns.
173
+ #
174
+ # INPUT:
175
+ # +test_path+ filesystem path to store all test data
176
+ #
177
+ def prepare_test(test_path)
178
+
179
+ Stella::LOGGER.info("Writing test data to: #{test_path}\n\n") unless @quiet
180
+
181
+ # Make sure the test storage directory is created along with the
182
+ # latest symlink
183
+ FileUtil.create_dir(test_path)
184
+ File.unlink(latest_test_symlink_path) if File.exists?(latest_test_symlink_path)
185
+ File.symlink(File.expand_path(test_path), latest_test_symlink_path)
186
+
187
+ # Write the test ID to the storage directory
188
+ FileUtil.write_file(test_path + "/ID.txt", @guid, true)
189
+
190
+ # And the test run message
191
+ FileUtil.write_file(test_path + "/MESSAGE.txt", @testdef.message, true) if @testdef.message
192
+
193
+ @adapter.user_agent = @agents unless @agents.empty?
194
+
195
+ # The warmup is identical to a testrun except for two things:
196
+ # - we don't make note of the runpath
197
+ # - there is a one second sleep after the run.
198
+ # Everything else is identical.
199
+ if @testdef.warmup
200
+
201
+ testrun = maker_of_testruns(@testdef.warmup)
202
+
203
+ # Generate the filesystem path to store the run data.
204
+ # NOTE: We don't keep the warmup path in @test_runpaths because we
205
+ # include it in the final calculation for the test.
206
+ runpath = File.join(test_path, "warmup")
207
+
208
+ # Run the warmpup round
209
+ testrun.call("Warmup", runpath)
210
+
211
+ run_sleeper(@testdef.sleep || 1)
212
+
213
+ Stella::LOGGER.info('', '') unless @quiet # We just need the newline
214
+
215
+ end
216
+
217
+ print_title unless @quiet
218
+
219
+ end
220
+
221
+ # maker_of_testruns
222
+ #
223
+ # Generator of test runs. Everything that happens during a test
224
+ # run is defined here. We use a Proc so we can toss the functionality
225
+ # around like something that's dirty and loves a good tossing.
226
+ # It's important that all output be on a single line without a
227
+ # line terminator. Otherwise great descruction could occur.
228
+ def maker_of_testruns(factor)
229
+ testrun = Proc.new do |name,runpath|
230
+ # Make sure the test run storage directory is created
231
+ FileUtil.create_dir(runpath)
232
+
233
+ @adapter.load_factor = factor
234
+ @adapter.working_directory = runpath
235
+ @adapter.before
236
+
237
+ Stella::LOGGER.info_printf("%8s: %10d@%-6s ", name, @adapter.requests, @adapter.vuser_rate) unless @quiet
238
+
239
+ # Here we record the command arguments. This needs to be last because we modify
240
+ # some of the arguments above.
241
+ FileUtil.write_file(runpath + "/COMMAND.txt", @adapter.command, true)
242
+
243
+ # Execute the command, send STDOUT and STDERR to separate files.
244
+ command = "#{@adapter.command} 1> '#{@adapter.stdout_path}' 2> '#{@adapter.stderr_path}'"
245
+ Stella::LOGGER.info(" COMMAND: #{command}") if @verbose >= 2
246
+
247
+ # Call the load tool
248
+ # for Windows, see: http://blog.crankybit.com/redirecting-output-to-a-file-in-windows-batch-scripts/
249
+ # http://weblogs.asp.net/lorenh/archive/2006/03/24/441004.aspx
250
+ system(command)
251
+
252
+ stats = @adapter.stats
253
+
254
+ save_summary(@adapter.summary_path(@format), stats)
255
+
256
+
257
+ unless @quiet
258
+ Stella::LOGGER.info_print(sprintf("%3.0f%% %9.2f/s %8.3fs ", stats.availability, stats.transaction_rate, stats.response_time))
259
+ Stella::LOGGER.info_print(sprintf("%8.3fMB/s %8.3fMB %8.3fs ", stats.throughput, stats.data_transferred, stats.elapsed_time))
260
+ # NOTE: We don't print a line terminator here
261
+ end
262
+
263
+ end
264
+ end
265
+
266
+
267
+ # print_summary
268
+ #
269
+ # Called during the test after every batch of test runs. For a rampup test
270
+ # it's also called at the end of all the runs.
271
+ #
272
+ # INPUT:
273
+ # stats:: Any object that extends Stella::Test::Base object
274
+ def print_summary(stats)
275
+ Stella::LOGGER.info(' ' << "-"*67) unless @quiet
276
+
277
+ Stella::LOGGER.info_printf("%8s: %10d@%-6d% 3.0f%% %9.2f/s ", "Total", stats.transactions_total, stats.vusers_avg, stats.availability, stats.transaction_rate_avg)
278
+ Stella::LOGGER.info_printf("%8.3fs %8.3fMB/s %8.3fMB %8.3fs", stats.response_time_avg, stats.throughput_avg, stats.data_transferred_total, stats.elapsed_time_total)
279
+ Stella::LOGGER.info('') # New line
280
+ Stella::LOGGER.info_printf("%8s: %22s %9.2f/s %8.3fs %8.3fMB/s %10s %8.3fs", "Std Dev", '', stats.transaction_rate_sdev, stats.response_time_sdev, stats.throughput_sdev, '', stats.elapsed_time_sdev)
281
+ Stella::LOGGER.info('') # New line
282
+ Stella::LOGGER.info('') unless @quiet # Extra new line
283
+ end
284
+
285
+ # print_title
286
+ #
287
+ # Prints the column headers for the test run output. Field widths match those
288
+ # in print_summary and test_maker
289
+ def print_title
290
+ Stella::LOGGER.info(' ' << "-"*67) unless @quiet
291
+
292
+ Stella::LOGGER.info_printf("%8s %10s@%-5s %5s %11s", "", 'REQ', 'VU/s', 'AVAIL', 'REQ/s')
293
+ Stella::LOGGER.info_printf("%10s %12s %10s %9s", 'RTIME', 'DATA/s', 'DATA', 'TIME')
294
+ Stella::LOGGER.info('') # New line
295
+ Stella::LOGGER.info('') unless @quiet # Extra new line
296
+ end
297
+
298
+ # save_summary
299
+ #
300
+ # Write a summary object to disk
301
+ #
302
+ # INPUT:
303
+ # filepath:: the complete path for the file (string)
304
+ # stats:: Any object that extends Stella::Test::Base object
305
+ def save_summary(filepath, stats)
306
+ FileUtil.write_file(filepath, stats.dump(@format, :with_titles), true)
307
+ end
308
+
309
+ # Load SUMMARY file for each run and create a summary with
310
+ # totals, averages, and standard deviations.
311
+ def process_test_stats(paths)
312
+ test_stats = Stella::Test::Summary.new(@message)
313
+ all_stats_obj = []
314
+ paths.each do |path|
315
+ file_contents = FileUtil.read_file_to_array("#{path}/SUMMARY.#{@format}")
316
+ stats = Stella::Test::Run::Summary.undump(@format, file_contents)
317
+ stats_obj = Stella::Test::Run::Summary.from_hash(stats)
318
+ test_stats.add_run(stats_obj)
319
+ end
320
+
321
+ test_stats
322
+ end
323
+
324
+ # This is the path where all test data will be stored
325
+ # The default is testruns/2008-12-31/test-001
326
+ def _generate_test_path
327
+ now = DateTime.now
328
+ time = Time.now
329
+ daystr = "#{now.year}-#{now.mon.to_s.rjust(2,'0')}-#{now.mday.to_s.rjust(2,'0')}"
330
+ testpath = File.join(@working_directory, 'testruns', daystr, 'test-')
331
+ testnum = 1.to_s.rjust(3,'0')
332
+ testnum.succ! while(File.directory? "#{testpath}#{testnum}")
333
+ "#{testpath}#{testnum}"
334
+ end
335
+
336
+
337
+ end
338
+ end
339
+