front_end_loader 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2011-2012 Brewster Inc., Aubrey Holland
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # Front End Loader
2
+
3
+ Front End Loader is a Ruby DSL for declaring load tests. It works in the spirit of
4
+ tools like JMeter, by simulating a number of users performing a scripted set of actions
5
+ and displaying metrics about response times and error rates as the requests are performed.
6
+ Unlike GUI tools like JMeter, however, front_end_loader makes it very simple to declare
7
+ your requests and to pass data between requests, by looking at the responses to gather data.
8
+
9
+ ## Install
10
+ gem install front_end_loader
11
+
12
+ ## Creating an Experiment
13
+
14
+ In order to create a test, just declare a FrontEndLoader::Experiment object:
15
+
16
+ ```ruby
17
+ require 'front_end_loader'
18
+
19
+ experiment = FrontEndLoader::Experiment.new.tap do |e|
20
+ e.user_count = 20
21
+ e.loop_count = 5
22
+ e.domain = 'https://www.google.com'
23
+ e.basic_auth('unreal_login', 'unreal_password')
24
+ e.default_parameters = { 'unnecessary' => 'true' }
25
+ e.debug = '/tmp/front_end_loader.txt'
26
+
27
+ e.requests do |r|
28
+ ...
29
+ end
30
+ end
31
+
32
+ experiment.run
33
+ ```
34
+
35
+ This block declares an experiment that:
36
+
37
+ * simulates 20 users simultaneously interacting with the system
38
+ * executes the request script five times per user before exiting. You can specify infinite loops by either not calling loop_count or passing -1
39
+ * will operate against the google.com domain
40
+ * uses http basic auth
41
+ * passes a default parameter of unnecessary to each request, and
42
+ * writes debugging output to /tmp/front_end_loader.txt
43
+
44
+ It then runs the experiment, which causes the requests to start flowing and output to be displayed
45
+ on the screen. The requests method on the experiment is where you will define the script to be run
46
+ loop_count times for each of the simulated users:
47
+
48
+ ```ruby
49
+ e.requests do |r|
50
+
51
+ word = nil
52
+
53
+ r.get('test_search', '/search', :q => 'test') do |response|
54
+ word = response.body.
55
+ split(/\s/).
56
+ reject { |i| i.length < 3 || i.length > 10 }.
57
+ sort_by { rand }.
58
+ first
59
+ end
60
+
61
+ e.write_debug(word)
62
+
63
+ r.get('random_word_search', '/search', :q => word)
64
+
65
+ r.get('privacy_policy', '/intl/en/policies')
66
+
67
+ # r.post(...)
68
+ # r.put(...)
69
+ # r.delete(...)
70
+ end
71
+ ```
72
+
73
+ For each request, arguments are:
74
+
75
+ * the label to use when tracking it in the display
76
+ * the path
77
+ * parameters, as a hash
78
+ * for post and put requests, a data object to use as the request body
79
+
80
+ All request declarations can take a block that will be passed the response from that request. The response
81
+ is a Patron::Response object and can be used to access data and pass it into further requests. Each iteration
82
+ of the script will be run in order and will not affect other iterations that may be running.
83
+
84
+ ## Running the experiment
85
+
86
+ Excuting an experiment will produce output like this:
87
+
88
+ ```
89
+ ------------------------------------------------------------------------------------------------------
90
+ | call | count | avg time | max time | errors | error % | throughput |
91
+ ------------------------------------------------------------------------------------------------------
92
+ | profile | 40 | 0.252 | 0.731 | 0 | 0.0 | 140 |
93
+ | random search | 40 | 0.275 | 0.491 | 0 | 0.0 | 140 |
94
+ | filtered_search | 40 | 0.28 | 0.67 | 0 | 0.0 | 140 |
95
+ | suggestions | 40 | 0.264 | 0.624 | 0 | 0.0 | 140 |
96
+ | autocomplete | 38 | 0.234 | 0.456 | 0 | 0.0 | 133 |
97
+ | filtered autocomplete | 37 | 0.204 | 0.323 | 0 | 0.0 | 130 |
98
+ | services | 37 | 0.203 | 0.476 | 0 | 0.0 | 130 |
99
+ | service types | 37 | 0.185 | 0.456 | 0 | 0.0 | 130 |
100
+ | me | 36 | 0.25 | 0.555 | 0 | 0.0 | 126 |
101
+ | | | | | | | |
102
+ | TOTAL | 345 | 0.238 | 0.731 | 0 | 0.0 | 1209 |
103
+ ------------------------------------------------------------------------------------------------------
104
+ run time: 0:00:17
105
+ ```
106
+
107
+ Throughput is measured in requests per minute and note that because each "user" is running though the script
108
+ in series, the throughput for an individual request is not as high as you would expect by running only that request
109
+ over and over again.
110
+
111
+ This display accepts the following keyboard controls:
112
+
113
+ * c - reset the data
114
+ * d - write the contents of the screen to the debug file
115
+ * p - pause the scripts, so the data will remain static and no requests will be made
116
+ * q - quit
117
+ * s - start the scripts again when paused
118
+
119
+ ## <a name="copyright"></a>Copyright
120
+ Copyright (c) 2011-2012 Brewster Inc., Aubrey Holland
121
+ See [LICENSE](https://github.com/brewster/front_end_loader/blob/master/LICENSE) for details.
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ task :default => :test
4
+
5
+ ## helper functions
6
+
7
+ def name
8
+ @name ||= Dir['*.gemspec'].first.split('.').first
9
+ end
10
+
11
+ def version
12
+ line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
13
+ line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
14
+ end
15
+
16
+ def gemspec_file
17
+ "#{name}.gemspec"
18
+ end
19
+
20
+ def replace_header(head, header_name)
21
+ head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
22
+ end
23
+
24
+ ## standard tasks
25
+
26
+ RSpec::Core::RakeTask.new(:test)
27
+
28
+ desc "Generate #{gemspec_file}"
29
+ task :gemspec do
30
+ # read spec file and split out manifest section
31
+ spec = File.read(gemspec_file)
32
+ head, manifest, tail = spec.split(" # = MANIFEST =\n")
33
+
34
+ # replace name version and date
35
+ replace_header(head, :name)
36
+ replace_header(head, :version)
37
+
38
+ # determine file list from git ls-files
39
+ files = `git ls-files`.
40
+ split("\n").
41
+ sort.
42
+ reject { |file| file =~ /^\./ }.
43
+ reject { |file| file =~ /^(rdoc|pkg)/ }.
44
+ map { |file| " #{file}" }.
45
+ join("\n")
46
+
47
+ # piece file back together and write
48
+ manifest = " s.files = %w[\n#{files}\n ]\n"
49
+ spec = [head, manifest, tail].join(" # = MANIFEST =\n")
50
+ File.open(gemspec_file, 'w') { |io| io.write(spec) }
51
+ puts "Updated #{gemspec_file}"
52
+ end
@@ -0,0 +1,36 @@
1
+ Gem::Specification.new do |s|
2
+ s.specification_version = 2 if s.respond_to? :specification_version=
3
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.3.5") if s.respond_to? :required_rubygems_version=
4
+
5
+ s.name = 'front_end_loader'
6
+ s.version = '0.2.3'
7
+
8
+ s.summary = 'A framework for doing declarative load testing in ruby'
9
+ s.description = <<-EOF
10
+ Front End Loader allows clients to declare load tests using a pure-Ruby DSL.
11
+ This means that it is very simple to pass data between requests or to interact
12
+ with your systems in dynamic, complex ways.
13
+ EOF
14
+ s.authors = ['Aubrey Holland']
15
+ s.email = 'aubreyholland@gmail.com'
16
+ s.homepage = 'https://github.com/brewster/front_end_loader'
17
+
18
+ s.add_dependency 'patron'
19
+
20
+ # = MANIFEST =
21
+ s.files = %w[
22
+ Gemfile
23
+ LICENSE
24
+ README.md
25
+ Rakefile
26
+ front_end_loader.gemspec
27
+ lib/front_end_loader.rb
28
+ lib/front_end_loader/experiment.rb
29
+ lib/front_end_loader/request.rb
30
+ lib/front_end_loader/request_manager.rb
31
+ lib/front_end_loader/screen.rb
32
+ ]
33
+ # = MANIFEST =
34
+
35
+ s.test_files = s.files.select { |path| path =~ %r{^spec/*/.+\.rb} }
36
+ end
@@ -0,0 +1,277 @@
1
+ module FrontEndLoader
2
+ class Experiment
3
+ attr_accessor :domain
4
+ attr_accessor :user_count
5
+ attr_accessor :loop_count
6
+ attr_accessor :default_parameters
7
+ attr_reader :basic_auth_enabled
8
+ attr_reader :basic_auth_user
9
+ attr_reader :basic_auth_password
10
+ attr_reader :screen
11
+
12
+ attr_reader :run_start_time
13
+ attr_reader :run_completed_time
14
+ attr_reader :running
15
+ attr_reader :call_times
16
+ attr_reader :call_error_counts
17
+ attr_reader :call_max_times
18
+
19
+ def initialize
20
+ @screen = Screen.new(self)
21
+ @running = false
22
+ @mutex = Mutex.new
23
+ @debug_mutex = Mutex.new
24
+ @loop_count = -1
25
+ @paused = false
26
+ @run_completed_time = nil
27
+ clear_data
28
+ end
29
+
30
+ def synchronize(&block)
31
+ @mutex.synchronize do
32
+ block.call
33
+ end
34
+ end
35
+
36
+ def debug=(file)
37
+ @debug_file = File.open(file, 'a')
38
+ end
39
+
40
+ def write_debug(data)
41
+ if @debug_file
42
+ @debug_mutex.synchronize do
43
+ @debug_file.puts(data)
44
+ @debug_file.flush
45
+ end
46
+ end
47
+ end
48
+
49
+ def write_screen_to_debug
50
+ if @debug_file
51
+ @debug_mutex.synchronize do
52
+ @screen.write_debug(@debug_file)
53
+ end
54
+ end
55
+ end
56
+
57
+ def clear_data
58
+ @mutex.synchronize do
59
+ @call_counts ||= Hash.new { |h,k| h[k] = 0 }
60
+ @call_counts.keys.each { |k| @call_counts[k] = 0 }
61
+
62
+ @call_times ||= Hash.new { |h,k| h[k] = 0.0 }
63
+ @call_times.keys.each { |k| @call_times[k] = 0.0 }
64
+
65
+ @call_max_times ||= Hash.new { |h,k| h[k] = 0.0 }
66
+ @call_max_times.keys.each { |k| @call_max_times[k] = 0.0 }
67
+
68
+ @call_error_counts ||= Hash.new { |h,k| h[k] = 0 }
69
+ @call_error_counts .keys.each { |k| @call_error_counts[k] = 0 }
70
+
71
+ @call_times_last_25 ||= Hash.new { |h,k| h[k] = [] }
72
+ @call_times_last_25.keys.each { |k| @call_times_last_25[k] = [] }
73
+
74
+ @error_counts_by_type ||= Hash.new { |h,k| h[k] = 0 }
75
+ @error_counts_by_type.keys.each { |k| @error_counts_by_type[k] = 0 }
76
+
77
+ if @run_start_time
78
+ @run_start_time = Time.now
79
+ else
80
+ @run_start_time = nil
81
+ end
82
+ end
83
+ end
84
+
85
+ def basic_auth(user, password)
86
+ @basic_auth_enabled = true
87
+ @basic_auth_user = user
88
+ @basic_auth_password = password
89
+ end
90
+
91
+ def requests(&block)
92
+ @request_block = block
93
+ end
94
+
95
+ def run_completed!
96
+ @run_completed_time = Time.now
97
+ end
98
+
99
+ def run
100
+ @running = true
101
+ @run_start_time = Time.now
102
+
103
+ threads = (1..user_count).to_a.map do
104
+ Thread.new(self, @request_block) do |experiment, request_block|
105
+ loops_left = experiment.loop_count
106
+ while(loops_left != 0)
107
+ if experiment.paused?
108
+ sleep(0.25)
109
+ elsif experiment.quitting?
110
+ loops_left = 0
111
+ else
112
+ request_manager = RequestManager.new(experiment, experiment.http_session)
113
+ request_block.call(request_manager)
114
+ loops_left -= 1
115
+ end
116
+ end
117
+ experiment.run_completed!
118
+ end
119
+ end
120
+
121
+ threads << Thread.new(self) do |experiment|
122
+ while (!experiment.quitting?)
123
+ if experiment.paused?
124
+ sleep(0.25)
125
+ else
126
+ experiment.screen.refresh
127
+ sleep(0.1)
128
+ end
129
+ end
130
+ end
131
+
132
+ threads << Thread.new(self) do |experiment|
133
+ while (!experiment.quitting?)
134
+ ch = Curses.getch
135
+ if ch == 'c'
136
+ experiment.clear_data
137
+ elsif ch == 'd'
138
+ experiment.write_screen_to_debug
139
+ elsif ch == 'p'
140
+ experiment.pause
141
+ elsif ch == 'q'
142
+ experiment.quit
143
+ elsif ch == 's'
144
+ experiment.clear_data
145
+ experiment.go
146
+ end
147
+ end
148
+ end
149
+
150
+ begin
151
+ threads.each(&:run)
152
+ threads.each(&:join)
153
+ rescue Interrupt
154
+ @screen.close
155
+ end
156
+
157
+ @screen.close
158
+ end
159
+
160
+ def http_session
161
+ Patron::Session.new.tap do |session|
162
+ session.base_url = domain
163
+ session.insecure = true
164
+ session.max_redirects = 0
165
+ if basic_auth_enabled
166
+ session.auth_type = :basic
167
+ session.username = basic_auth_user
168
+ session.password = basic_auth_password
169
+ session.connect_timeout = 10
170
+ session.timeout = 500
171
+ end
172
+ end
173
+ end
174
+
175
+ def paused?
176
+ @paused
177
+ end
178
+
179
+ def pause
180
+ @paused = true
181
+ end
182
+
183
+ def go
184
+ @paused = false
185
+ end
186
+
187
+ def quitting?
188
+ @quitting
189
+ end
190
+
191
+ def quit
192
+ @quitting = true
193
+ end
194
+
195
+ def time_call(name, &block)
196
+ begin
197
+ start = Time.now
198
+ response = block.call
199
+ time = Time.now - start
200
+ @mutex.synchronize do
201
+ @call_times[name] += time
202
+ @call_max_times[name] = time if time > @call_max_times[name]
203
+ unless response.status >= 200 && response.status < 400
204
+ write_debug(response.body)
205
+ @call_error_counts[name] += 1
206
+ @error_counts_by_type[response.status] += 1
207
+ end
208
+ @call_counts[name] += 1
209
+ @call_times_last_25[name].unshift(time)
210
+ @call_times_last_25[name] = @call_times_last_25[name].slice(0, 25)
211
+ end
212
+ response
213
+ rescue Patron::TimeoutError
214
+ add_timeout(name)
215
+ end
216
+ end
217
+
218
+ def add_timeout(name)
219
+ @mutex.synchronize do
220
+ @call_counts[name] += 1
221
+ @call_times[name] += 0
222
+ @call_error_counts[name] += 1
223
+ @error_counts_by_type['Timeout'] += 1
224
+ end
225
+ end
226
+
227
+ def call_counts
228
+ @call_counts.dup
229
+ end
230
+
231
+ def total_times
232
+ @call_times.dup
233
+ end
234
+
235
+ def average_times
236
+ @call_times.keys.inject({}) do |hash, name|
237
+ if @call_counts[name] == 0
238
+ hash[name] = 0.0
239
+ else
240
+ hash[name] = @call_times[name] / @call_counts[name].to_f
241
+ end
242
+ hash
243
+ end
244
+ end
245
+
246
+ def max_times
247
+ @call_max_times.dup
248
+ end
249
+
250
+ def error_counts
251
+ @call_error_counts.dup
252
+ end
253
+
254
+ def error_types
255
+ @error_counts_by_type.dup
256
+ end
257
+
258
+ def error_percents
259
+ @call_counts.keys.inject({}) do |hash, name|
260
+ if @call_counts[name] && @call_counts[name] > 0 && @call_error_counts[name] && @call_error_counts[name] > 0
261
+ hash[name] = (@call_error_counts[name].to_f / @call_counts[name].to_f) * 100.0
262
+ else
263
+ hash[name] = 0.0
264
+ end
265
+ hash
266
+ end
267
+ end
268
+
269
+ def throughput
270
+ delta = ((@run_completed_time || Time.now) - @run_start_time) / 60.0
271
+ @call_counts.keys.inject({}) do |hash, name|
272
+ hash[name] = @call_counts[name].to_f / delta
273
+ hash
274
+ end
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,30 @@
1
+ module FrontEndLoader
2
+ class Request
3
+ def initialize(experiment, session, method, name, path, params, data, response_block)
4
+ @experiment = experiment
5
+ @session = session
6
+ @method = method
7
+ @name = name
8
+ @path = path
9
+ @params = URI.encode(@experiment.default_parameters.merge(params).map { |k,v| "#{k}=#{v}" }.join('&'))
10
+ @data = data
11
+ @response_block = response_block
12
+ end
13
+
14
+ def run
15
+ response = nil
16
+ if [:get, :delete].include?(@method)
17
+ response = @experiment.time_call(@name) do
18
+ @session.__send__(@method, "#{@path}?#{@params}")
19
+ end
20
+ else
21
+ response = @experiment.time_call(@name) do
22
+ @session.__send__(@method, "#{@path}?#{@params}", @data, {'Content-Type' => 'application/json'})
23
+ end
24
+ end
25
+ if @response_block && response.is_a?(Patron::Response)
26
+ @response_block.call(response)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ module FrontEndLoader
2
+ class RequestManager
3
+ def initialize(experiment, session)
4
+ @experiment = experiment
5
+ @session = session
6
+ end
7
+
8
+ def get(name, path, params={}, &block)
9
+ Request.new(@experiment, @session, :get, name, path, params, nil, block).run
10
+ end
11
+
12
+ def post(name, path, params={}, data="{}", &block)
13
+ Request.new(@experiment, @session, :post, name, path, params, data, block).run
14
+ end
15
+
16
+ def put(name, path, params={}, data="{}", &block)
17
+ Request.new(@experiment, @session, :put, name, path, params, data, block).run
18
+ end
19
+
20
+ def delete(name, path, params={}, &block)
21
+ Request.new(@experiment, @session, :delete, name, path, params, nil, block).run
22
+ end
23
+
24
+ def debug(data)
25
+ @experiment.write_debug(data)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,226 @@
1
+ require 'curses'
2
+
3
+ module FrontEndLoader
4
+ class Screen
5
+
6
+ POSITIONS = {
7
+ :count => 0,
8
+ :average_time => 11,
9
+ :max_time => 22,
10
+ :errors => 33,
11
+ :error_percent => 44,
12
+ :throughput => 55
13
+ }
14
+
15
+ LENGTHS = {
16
+ :count => 9,
17
+ :average_time => 9,
18
+ :max_time => 9,
19
+ :errors => 9,
20
+ :error_percent => 9,
21
+ :throughput => 12
22
+ }
23
+
24
+ TOTAL_LENGTH = 66
25
+
26
+ def initialize(experiment)
27
+ @experiment = experiment
28
+ @entry_count = nil
29
+ @longest_name = 0
30
+ Curses.init_screen
31
+ Curses.nonl
32
+ Curses.cbreak
33
+ Curses.noecho
34
+ end
35
+
36
+ def refresh
37
+ run_start_time = nil
38
+ names = nil
39
+ counts_by_name = {}
40
+ times_by_name = {}
41
+ average_times_by_name = {}
42
+ max_times_by_name = {}
43
+ error_counts_by_name = {}
44
+ error_counts_by_type = {}
45
+ error_percents_by_name = {}
46
+ throughput_by_name = {}
47
+ total_calls = nil
48
+ total_time = nil
49
+ total_errors = nil
50
+ delta = nil
51
+ max_max_time = nil
52
+
53
+ @experiment.synchronize do
54
+ return if !@experiment.running || @experiment.call_counts.empty?
55
+
56
+ run_start_time = @experiment.run_start_time.dup
57
+ names = @experiment.call_counts.keys.dup
58
+
59
+ counts_by_name = @experiment.call_counts
60
+ times_by_name = @experiment.total_times
61
+ average_times_by_name = @experiment.average_times
62
+ max_times_by_name = @experiment.max_times
63
+ error_counts_by_name = @experiment.error_counts
64
+ error_counts_by_type = @experiment.error_types
65
+ error_percents_by_name = @experiment.error_percents
66
+ throughput_by_name = @experiment.throughput
67
+
68
+ total_calls = @experiment.call_counts.values.inject(0) { |s,i| s + i }
69
+ total_time = @experiment.call_times.values.inject(0) { |s,i| s + i }
70
+ total_errors = @experiment.call_error_counts.values.inject(0) { |s,i| s + i }
71
+ max_max_time = @experiment.call_max_times.values.max.round(3).to_s
72
+ delta = ((@experiment.run_completed_time || Time.now) - @experiment.run_start_time) / 60.0
73
+ end
74
+
75
+ if names.count != @entry_count
76
+ @entry_count = names.length
77
+ @longest_name = names.map(&:length).max
78
+ draw_outlines(names)
79
+ end
80
+
81
+ first_position = @longest_name + 6
82
+ line = 3
83
+ names.each do |name|
84
+ clear_line(line, first_position)
85
+ Curses.setpos(line, first_position + POSITIONS[:count])
86
+ Curses.addstr(counts_by_name[name].to_s)
87
+ Curses.setpos(line, first_position + POSITIONS[:average_time])
88
+ Curses.addstr(average_times_by_name[name].round(3).to_s)
89
+ Curses.setpos(line, first_position + POSITIONS[:max_time])
90
+ Curses.addstr(max_times_by_name[name].round(3).to_s)
91
+ Curses.setpos(line, first_position + POSITIONS[:errors])
92
+ Curses.addstr(error_counts_by_name[name].to_s)
93
+ Curses.setpos(line, first_position + POSITIONS[:error_percent])
94
+ Curses.addstr(error_percents_by_name[name].round(3).to_s)
95
+ Curses.setpos(line, first_position + POSITIONS[:throughput])
96
+ Curses.addstr(throughput_by_name[name].to_i.to_s)
97
+ line += 1
98
+ end
99
+ line += 1
100
+ clear_line(line, first_position)
101
+ Curses.setpos(line, first_position + POSITIONS[:count])
102
+ Curses.addstr(total_calls.to_s)
103
+ Curses.setpos(line, first_position + POSITIONS[:average_time])
104
+ Curses.addstr((total_time / total_calls.to_f).round(3).to_s)
105
+ Curses.setpos(line, first_position + POSITIONS[:max_time])
106
+ Curses.addstr(max_max_time.to_s)
107
+ Curses.setpos(line, first_position + POSITIONS[:errors])
108
+ Curses.addstr(total_errors.to_s)
109
+ Curses.setpos(line, first_position + POSITIONS[:error_percent])
110
+ Curses.addstr(((total_errors.to_f / total_calls.to_f) * 100.0).round(1).to_s)
111
+ Curses.setpos(line, first_position + POSITIONS[:throughput])
112
+ Curses.addstr((total_calls.to_f / delta).to_i.to_s)
113
+
114
+ line += 3
115
+ time = Time.now - run_start_time
116
+ hours = (time / 3600).to_i
117
+ minutes = (time / 60 - (hours * 60)).to_i
118
+ seconds = (time - (minutes * 60 + hours * 3600)).to_i
119
+ Curses.setpos(line, 3)
120
+ Curses.addstr("run time: #{hours}:#{'%02d' % minutes}:#{'%02d' % seconds}")
121
+
122
+ line += 2
123
+ error_counts_by_type.each do |type, count|
124
+ erase_line(line)
125
+ Curses.setpos(line, 3)
126
+ Curses.addstr("#{type}: #{count}")
127
+ line += 1
128
+ end
129
+
130
+ Curses.curs_set(0)
131
+ Curses.refresh
132
+ rescue StandardError => e
133
+ puts e.message
134
+ puts e.backtrace.first
135
+ end
136
+
137
+ def clear_line(line, first_position)
138
+ Curses.setpos(line, first_position + POSITIONS[:count])
139
+ Curses.addstr(' ' * LENGTHS[:count])
140
+ Curses.setpos(line, first_position + POSITIONS[:average_time])
141
+ Curses.addstr(' ' * LENGTHS[:average_time])
142
+ Curses.setpos(line, first_position + POSITIONS[:max_time])
143
+ Curses.addstr(' ' * LENGTHS[:max_time])
144
+ Curses.setpos(line, first_position + POSITIONS[:errors])
145
+ Curses.addstr(' ' * LENGTHS[:error_percent])
146
+ Curses.setpos(line, first_position + POSITIONS[:error_percent])
147
+ Curses.addstr(' ' * LENGTHS[:error_percent])
148
+ Curses.setpos(line, first_position + POSITIONS[:throughput])
149
+ Curses.addstr(' ' * LENGTHS[:throughput])
150
+ end
151
+
152
+ def erase_line(line)
153
+ Curses.setpos(line, 0)
154
+ Curses.addstr(' ' * TOTAL_LENGTH)
155
+ end
156
+
157
+ def draw_outlines(names)
158
+ first_position = @longest_name + 6
159
+ Curses.clear
160
+ Curses.setpos(0, 2)
161
+ Curses.addstr('-' * (first_position + TOTAL_LENGTH))
162
+ Curses.setpos(1, 1)
163
+ Curses.addstr(divider_string)
164
+ Curses.setpos(1, 3)
165
+ Curses.addstr('call')
166
+ Curses.setpos(1, first_position + POSITIONS[:count])
167
+ Curses.addstr('count')
168
+ Curses.setpos(1, first_position + POSITIONS[:average_time])
169
+ Curses.addstr('avg time')
170
+ Curses.setpos(1, first_position + POSITIONS[:max_time])
171
+ Curses.addstr('max time')
172
+ Curses.setpos(1, first_position + POSITIONS[:errors])
173
+ Curses.addstr('errors')
174
+ Curses.setpos(1, first_position + POSITIONS[:error_percent])
175
+ Curses.addstr('error %')
176
+ Curses.setpos(1, first_position + POSITIONS[:throughput])
177
+ Curses.addstr('throughput')
178
+ Curses.setpos(2, 2)
179
+ Curses.addstr('-' * (first_position + TOTAL_LENGTH))
180
+ line = 3
181
+ names.each do |name|
182
+ Curses.setpos(line, 1)
183
+ Curses.addstr(divider_string)
184
+ Curses.setpos(line, 3)
185
+ Curses.addstr(name)
186
+ line += 1
187
+ end
188
+ Curses.setpos(line, 1)
189
+ Curses.addstr(divider_string)
190
+ line += 1
191
+ Curses.setpos(line, 1)
192
+ Curses.addstr(divider_string)
193
+
194
+ Curses.setpos(line, 3)
195
+ Curses.addstr('TOTAL')
196
+ line += 1
197
+ Curses.setpos(line, 2)
198
+ Curses.addstr('-' * (first_position + TOTAL_LENGTH))
199
+ end
200
+
201
+ def divider_string
202
+ first_position = '|' + (' ' * (@longest_name + 2))
203
+ first_position + '| | | | | | |'
204
+ end
205
+
206
+ def close
207
+ Curses.curs_set(1)
208
+ Curses::close_screen
209
+ end
210
+
211
+ def write_debug(file)
212
+ (0..Curses.lines).each do |line|
213
+ line_string = ''
214
+ (0..Curses.cols).each do |col|
215
+ Curses.setpos(line, col)
216
+ line_string << Curses.inch.chr
217
+ end
218
+ line_string.strip!
219
+ unless line_string.empty?
220
+ file.puts(line_string)
221
+ end
222
+ end
223
+ file.flush
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,10 @@
1
+ require 'patron'
2
+ require 'uri'
3
+
4
+ %w(experiment request request_manager screen).each do |file|
5
+ require_relative "front_end_loader/#{file}"
6
+ end
7
+
8
+ module FrontEndLoader
9
+ VERSION = '0.2.2'
10
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: front_end_loader
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.3
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Aubrey Holland
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: patron
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: ! 'Front End Loader allows clients to declare load tests using a pure-Ruby
31
+ DSL.
32
+
33
+ This means that it is very simple to pass data between requests or to interact
34
+
35
+ with your systems in dynamic, complex ways.
36
+
37
+ '
38
+ email: aubreyholland@gmail.com
39
+ executables: []
40
+ extensions: []
41
+ extra_rdoc_files: []
42
+ files:
43
+ - Gemfile
44
+ - LICENSE
45
+ - README.md
46
+ - Rakefile
47
+ - front_end_loader.gemspec
48
+ - lib/front_end_loader.rb
49
+ - lib/front_end_loader/experiment.rb
50
+ - lib/front_end_loader/request.rb
51
+ - lib/front_end_loader/request_manager.rb
52
+ - lib/front_end_loader/screen.rb
53
+ homepage: https://github.com/brewster/front_end_loader
54
+ licenses: []
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: 1.3.5
71
+ requirements: []
72
+ rubyforge_project:
73
+ rubygems_version: 1.8.24
74
+ signing_key:
75
+ specification_version: 2
76
+ summary: A framework for doing declarative load testing in ruby
77
+ test_files: []