front_end_loader 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.md +121 -0
- data/Rakefile +52 -0
- data/front_end_loader.gemspec +36 -0
- data/lib/front_end_loader/experiment.rb +277 -0
- data/lib/front_end_loader/request.rb +30 -0
- data/lib/front_end_loader/request_manager.rb +28 -0
- data/lib/front_end_loader/screen.rb +226 -0
- data/lib/front_end_loader.rb +10 -0
- metadata +77 -0
data/Gemfile
ADDED
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
|
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: []
|