autograph 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +26 -0
- data/History.txt +4 -0
- data/README.rdoc +93 -0
- data/Rakefile +33 -0
- data/TODO +32 -0
- data/VERSION +1 -0
- data/bin/autograph +149 -0
- data/lib/autograph.rb +15 -0
- data/lib/autograph/autoperf.rb +105 -0
- data/lib/autograph/configuration.rb +63 -0
- data/lib/autograph/graph_renderers/base_renderer.rb +76 -0
- data/lib/autograph/graph_renderers/gchart_renderer.rb +82 -0
- data/lib/autograph/graph_renderers/rasterized_scruffy_renderer.rb +20 -0
- data/lib/autograph/graph_renderers/scruffy_renderer.rb +31 -0
- data/lib/autograph/graph_series.rb +16 -0
- data/lib/autograph/html_report.rb +33 -0
- data/lib/autograph/report.html.erb +174 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/test/test_autograph.rb +18 -0
- data/test/test_configuration.rb +29 -0
- data/test/test_graph_series.rb +30 -0
- data/test/test_helper.rb +3 -0
- data/test/test_html_report.rb +38 -0
- metadata +147 -0
data/.gitignore
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
## MAC OS
|
2
|
+
.DS_Store
|
3
|
+
|
4
|
+
## TEXTMATE
|
5
|
+
*.tmproj
|
6
|
+
tmtags
|
7
|
+
|
8
|
+
## EMACS
|
9
|
+
*~
|
10
|
+
\#*
|
11
|
+
.\#*
|
12
|
+
|
13
|
+
## VIM
|
14
|
+
*.swp
|
15
|
+
|
16
|
+
## PROJECT::GENERAL
|
17
|
+
coverage
|
18
|
+
rdoc
|
19
|
+
pkg
|
20
|
+
webgen*
|
21
|
+
|
22
|
+
## PROJECT::SPECIFIC
|
23
|
+
load_test*
|
24
|
+
eapis*
|
25
|
+
*.svg
|
26
|
+
*.gemspec
|
data/History.txt
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
= autograph
|
2
|
+
|
3
|
+
== DESCRIPTION:
|
4
|
+
|
5
|
+
Autograph drives httperf, varying the request rate and graphing the output. This exercise provides graphical data showing how the requested resources hold up under an increasing rate, particularly with request to response time and achieved request rate.
|
6
|
+
|
7
|
+
Can work with SOAP.
|
8
|
+
|
9
|
+
== NOTES:
|
10
|
+
|
11
|
+
A note on stats, from Zed Shaw: http://zedshaw.com/essays/programmer_stats.html
|
12
|
+
|
13
|
+
== PROBLEMS:
|
14
|
+
|
15
|
+
* No 'AcceptEncoding' support
|
16
|
+
* Limited to the load generation capabilities of a single server.
|
17
|
+
|
18
|
+
== SYNOPSIS:
|
19
|
+
|
20
|
+
autograph --host example.com --low-rate 5 --high-rate 25 --rate-step 5 --num-conns 25
|
21
|
+
|
22
|
+
autograph --host example.com --low-rate 10 --high-rate 50 --rate-step 10 --output-file my_load_test.html
|
23
|
+
|
24
|
+
== INSTALL:
|
25
|
+
|
26
|
+
* Get httperf installed ('brew install httperf' if you've got homebrew installed)
|
27
|
+
* Download project and run 'rake build install'
|
28
|
+
|
29
|
+
nick-stielaus-computer-3:autograph nick$ httperf -h
|
30
|
+
Usage: httperf [-hdvV] [--add-header S] [--burst-length N] [--client N/N]
|
31
|
+
[--close-with-reset] [--debug N] [--failure-status N]
|
32
|
+
[--help] [--hog] [--http-version S] [--max-connections N]
|
33
|
+
[--max-piped-calls N] [--method S] [--no-host-hdr]
|
34
|
+
[--num-calls N] [--num-conns N] [--period [d|u|e]T1[,T2]]
|
35
|
+
[--port N] [--print-reply [header|body]] [--print-request [header|body]]
|
36
|
+
[--rate X] [--recv-buffer N] [--retry-on-failure] [--send-buffer N]
|
37
|
+
[--server S] [--server-name S] [--session-cookies]
|
38
|
+
[--ssl] [--ssl-ciphers L] [--ssl-no-reuse]
|
39
|
+
[--think-timeout X] [--timeout X] [--uri S] [--verbose] [--version]
|
40
|
+
[--wlog y|n,file] [--wsess N,N,X] [--wsesslog N,X,file]
|
41
|
+
[--wset N,X]
|
42
|
+
nick-stielaus-computer-3:autograph nick$ autograph
|
43
|
+
Usage: autograph [options]
|
44
|
+
--host HOST The host to load test
|
45
|
+
--port PORT The port to load test
|
46
|
+
--uris PATH,PATH A comma separated list of pages to cycle through
|
47
|
+
--output-file PATH Specify the file to output to.
|
48
|
+
--output-dir PATH Specify a directory to write output files to.
|
49
|
+
--notes NOTES Notes to be written to the report.
|
50
|
+
--test Do not run benchmarks. Use test data to generate reports.
|
51
|
+
Httperf Knobs:
|
52
|
+
--timeout SECONDS The length in seconds before a request is marked as errored
|
53
|
+
--num-call NUMCALLS The number of calls to make for each connection in the test (defaults to one).
|
54
|
+
--num-conns NUMCONNS The number of connections to make for each test
|
55
|
+
--low-rate LOWRATE The starting rate
|
56
|
+
--high-rate HIGHRATE The highest rate at which to perform a test
|
57
|
+
--rate-step RATESTEP The ammount at which to increment the rate for each interation of the test
|
58
|
+
--wsesslog PATH Path to the wsesslog file.
|
59
|
+
Common options:
|
60
|
+
-h, --help Displays this help info
|
61
|
+
-v, --verbose Verbose output
|
62
|
+
|
63
|
+
|
64
|
+
== THANKS:
|
65
|
+
|
66
|
+
Thanks to
|
67
|
+
|
68
|
+
Julian T J Midgley
|
69
|
+
Autobench
|
70
|
+
http://www.xenoclast.org/autobench/
|
71
|
+
|
72
|
+
Ilya Grigorik
|
73
|
+
Autoperf
|
74
|
+
http://github.com/igrigorik/autoperf/tree
|
75
|
+
|
76
|
+
HP
|
77
|
+
httperf
|
78
|
+
http://www.hpl.hp.com/research/linux/httperf/
|
79
|
+
|
80
|
+
|
81
|
+
== LICENSE:
|
82
|
+
|
83
|
+
|
84
|
+
Autograph
|
85
|
+
Copyright (c) 2009 Nick Stielau
|
86
|
+
|
87
|
+
Based on ideas and code from autoperf.rb
|
88
|
+
http://github.com/igrigorik/autoperf/tree
|
89
|
+
Copyright (C)2008 Ilya Grigorik
|
90
|
+
|
91
|
+
|
92
|
+
|
93
|
+
You can redistribute this under the terms of the Ruby license
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require File.dirname(__FILE__) + '/lib/autograph'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "autograph"
|
8
|
+
gem.summary = %Q{Simple httperf runner with nice html reports with graphs.}
|
9
|
+
gem.description = %Q{Autograph wraps httperf, running multiple tests while varying the parameters, graphing the output.}
|
10
|
+
gem.email = "nick.stielau@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/nstielau/autograph"
|
12
|
+
gem.authors = ["Nick Stielau"]
|
13
|
+
gem.add_runtime_dependency 'builder', '= 2.1.2'
|
14
|
+
gem.add_runtime_dependency 'ruport', '= 1.6.3'
|
15
|
+
gem.add_runtime_dependency 'scruffy', '= 0.2.6'
|
16
|
+
gem.add_runtime_dependency 'gchart', '= 1.0.0'
|
17
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
18
|
+
end
|
19
|
+
Jeweler::GemcutterTasks.new
|
20
|
+
rescue LoadError
|
21
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
Rake::TestTask.new(:test) do |test|
|
26
|
+
test.libs << 'lib' << 'test'
|
27
|
+
test.pattern = 'test/**/test_*.rb'
|
28
|
+
test.verbose = true
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
task :test => :check_dependencies
|
33
|
+
task :default => :test
|
data/TODO
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# Report for AVG says "Report for http://127.0.0.1Avg"
|
2
|
+
# Skin report page
|
3
|
+
# Compare two hosts
|
4
|
+
# Config file?
|
5
|
+
|
6
|
+
# Deal with AcceptEncoding, add options
|
7
|
+
|
8
|
+
# Factor out outputs types into a report_renderers dir
|
9
|
+
# Control verbose output with a Logger, instead of just going to STDOUT
|
10
|
+
|
11
|
+
# Don't include overview for one page...
|
12
|
+
# Flag maxes
|
13
|
+
# Set report overview y-axis to the max request rate
|
14
|
+
|
15
|
+
# Pass-through httperf args
|
16
|
+
# Pick output format
|
17
|
+
# Pick graph types
|
18
|
+
# take a txt file for the urls
|
19
|
+
|
20
|
+
# Remove default timeout httperf option
|
21
|
+
# Add interactive mode
|
22
|
+
# Add badass testing server
|
23
|
+
|
24
|
+
# JS for selecting right view after page refresh
|
25
|
+
|
26
|
+
# Fix Graphs x=y!
|
27
|
+
|
28
|
+
# --group graphs, or separate into different pages
|
29
|
+
|
30
|
+
|
31
|
+
# Trap exit and write output to file? (Can you have two traps?)
|
32
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.2
|
data/bin/autograph
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'autograph'
|
5
|
+
|
6
|
+
options = {}
|
7
|
+
|
8
|
+
command_parts = [File.basename(__FILE__)]
|
9
|
+
|
10
|
+
opt_parse_opts = OptionParser.new do |opts|
|
11
|
+
opts.banner = "Usage: #{File.basename($0)} [options]"
|
12
|
+
|
13
|
+
opts.on("--host HOST", "The host to load test") do |opt|
|
14
|
+
options['host'] = opt
|
15
|
+
command_parts << "--host #{opt}"
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.on("--port PORT", Integer, "The port to load test") do |opt|
|
19
|
+
options['port'] = opt
|
20
|
+
command_parts << "--port #{opt}"
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on("--uris PATH,PATH", String, "A comma separated list of pages to cycle through") do |opt|
|
24
|
+
options['uris'] = opt.split(',')
|
25
|
+
command_parts << "--uris #{opt}"
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on("--output-file PATH", "Specify the file to output to.") do |opt|
|
29
|
+
options['output_file'] = opt
|
30
|
+
command_parts << "--output-file #{opt}"
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.on("--output-dir PATH", "Specify a directory to write output files to.") do |opt|
|
34
|
+
options["output_dir"] = opt;
|
35
|
+
command_parts << "--output-dir"
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.on("--notes NOTES", "Notes to be written to the report.") do |opt|
|
39
|
+
options['notes'] = opt
|
40
|
+
command_parts << "--notes '#{opt}'"
|
41
|
+
end
|
42
|
+
|
43
|
+
# opts.on("--average", "Run a round-robin test of all URIs") do
|
44
|
+
# options["average"] = true;
|
45
|
+
# command_parts << "--average"
|
46
|
+
# end
|
47
|
+
|
48
|
+
opts.on("--test", "Do not run benchmarks. Use test data to generate reports.") do |opt|
|
49
|
+
options['use_test_data'] = true
|
50
|
+
command_parts << "--test"
|
51
|
+
end
|
52
|
+
|
53
|
+
opts.separator ""
|
54
|
+
opts.separator "Httperf Knobs:"
|
55
|
+
|
56
|
+
opts.on("--timeout SECONDS", Integer, "The length in seconds before a request is marked as errored") do |opt|
|
57
|
+
options['httperf_timeout'] = opt
|
58
|
+
command_parts << "--timeout #{opt}"
|
59
|
+
end
|
60
|
+
|
61
|
+
opts.on("--num-call NUMCALLS", Integer, "The number of calls to make for each connection in the test (defaults to one).") do |opt|
|
62
|
+
options['httperf_num-call'] = opt
|
63
|
+
command_parts << "--num-call #{opt}"
|
64
|
+
end
|
65
|
+
|
66
|
+
opts.on("--num-conns NUMCONNS", Integer, "The number of connections to make for each test") do |opt|
|
67
|
+
options['httperf_num-conns'] = opt
|
68
|
+
command_parts << "--num-conns #{opt}"
|
69
|
+
end
|
70
|
+
|
71
|
+
# opts.on("--rate RATE", Integer, "The rate at which to make requests") do |opt|
|
72
|
+
# options['httperf_rate'] = opt
|
73
|
+
# command_parts << "--rate #{opt}"
|
74
|
+
# end
|
75
|
+
|
76
|
+
opts.on("--low-rate LOWRATE", Integer, "The starting rate") do |opt|
|
77
|
+
options['low_rate'] = opt
|
78
|
+
command_parts << "-low-rate #{opt}"
|
79
|
+
end
|
80
|
+
|
81
|
+
opts.on("--high-rate HIGHRATE", Integer, "The highest rate at which to perform a test") do |opt|
|
82
|
+
options['high_rate'] = opt
|
83
|
+
command_parts << "--high-rate #{opt}"
|
84
|
+
end
|
85
|
+
|
86
|
+
opts.on("--rate-step RATESTEP", Integer, "The ammount at which to increment the rate for each interation of the test") do |opt|
|
87
|
+
options['rate_step'] = opt
|
88
|
+
command_parts << "--rate-step #{opt}"
|
89
|
+
end
|
90
|
+
|
91
|
+
# opts.on("--wlog PATH", String, "A file a ASCII nul terminated pages to cycle through") do |opt|
|
92
|
+
# options['httperf_wlog'] = "y,#{opt}";
|
93
|
+
# command_parts << "--wlog #{opt}"
|
94
|
+
# end
|
95
|
+
|
96
|
+
opts.on("--wsesslog PATH", "Path to the wsesslog file.") do |opt|
|
97
|
+
options['httperf_wsesslog'] = opt
|
98
|
+
command_parts << "--wsesslog '#{opt}'"
|
99
|
+
end
|
100
|
+
|
101
|
+
opts.separator ""
|
102
|
+
opts.separator "Common options:"
|
103
|
+
|
104
|
+
opts.on("-h", "--help", "Displays this help info") do
|
105
|
+
puts opts
|
106
|
+
exit 0
|
107
|
+
end
|
108
|
+
|
109
|
+
opts.on("-v", "--verbose", "Verbose output") do
|
110
|
+
options["verbose"] = true;
|
111
|
+
command_parts << "-v"
|
112
|
+
end
|
113
|
+
|
114
|
+
begin
|
115
|
+
opts.parse!(ARGV)
|
116
|
+
rescue OptionParser::ParseError => e
|
117
|
+
warn e.message
|
118
|
+
puts opts
|
119
|
+
exit 1
|
120
|
+
end
|
121
|
+
|
122
|
+
opts
|
123
|
+
end
|
124
|
+
|
125
|
+
if options.length == 0
|
126
|
+
puts opt_parse_opts
|
127
|
+
exit
|
128
|
+
end
|
129
|
+
|
130
|
+
options["command_run"] = command_parts.join(" ")
|
131
|
+
|
132
|
+
trap("INT") {
|
133
|
+
abort("Terminating autograph.")
|
134
|
+
}
|
135
|
+
|
136
|
+
if options['verbose']
|
137
|
+
puts
|
138
|
+
puts "These args were specified from the command line:"
|
139
|
+
options.sort.each do |key, value|
|
140
|
+
puts " #{key}=#{value}"
|
141
|
+
end
|
142
|
+
puts
|
143
|
+
end
|
144
|
+
|
145
|
+
begin
|
146
|
+
AutoPerf.new(options)
|
147
|
+
rescue => e
|
148
|
+
abort(e.to_s)
|
149
|
+
end
|
data/lib/autograph.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'optparse'
|
5
|
+
require 'ruport'
|
6
|
+
require 'pp'
|
7
|
+
|
8
|
+
require 'autograph/autoperf'
|
9
|
+
require 'autograph/graph_series'
|
10
|
+
require 'autograph/html_report'
|
11
|
+
require 'autograph/configuration'
|
12
|
+
|
13
|
+
require 'autograph/graph_renderers/base_renderer'
|
14
|
+
require 'autograph/graph_renderers/gchart_renderer'
|
15
|
+
require 'autograph/graph_renderers/scruffy_renderer'
|
@@ -0,0 +1,105 @@
|
|
1
|
+
class AutoPerf
|
2
|
+
COLUMN_NAMES = ['rate', 'conn/s', 'req/s', 'replies/s avg', 'errors', 'net io (KB/s)', 'reply time']
|
3
|
+
|
4
|
+
def initialize(opts = {})
|
5
|
+
conf = Configuration.new(opts)
|
6
|
+
|
7
|
+
puts configuration.pretty_print if opts['verbose']
|
8
|
+
|
9
|
+
if conf['use_test_data']
|
10
|
+
conf['uris'] = ['/', '/page1', '/page2']
|
11
|
+
@reports = load_test_data(conf)
|
12
|
+
else
|
13
|
+
@reports = run_tests(conf)
|
14
|
+
end
|
15
|
+
|
16
|
+
graphs = BaseRenderer.generate_graphs(@reports, conf)
|
17
|
+
HtmlReport.new(@reports, graphs, conf)
|
18
|
+
end
|
19
|
+
|
20
|
+
def benchmark(conf)
|
21
|
+
raise "You must specify a host." if conf['host'].nil?
|
22
|
+
httperf_opt = conf.keys.grep(/httperf/).collect {|k| "--#{k.gsub(/httperf_/, '')} #{conf[k]}"}.join(" ")
|
23
|
+
httperf_cmd = "httperf --hog --server #{conf['host']} --port #{conf['port']} #{httperf_opt}"
|
24
|
+
|
25
|
+
res = Hash.new("")
|
26
|
+
IO.popen("#{httperf_cmd} 2>&1") do |pipe|
|
27
|
+
puts "\n#{httperf_cmd}"
|
28
|
+
|
29
|
+
while((line = pipe.gets))
|
30
|
+
res['output'] += line
|
31
|
+
|
32
|
+
case line
|
33
|
+
when /^Total: .*replies (\d+)/ then res['replies'] = $1
|
34
|
+
when /^Connection rate: (\d+\.\d)/ then res['conn/s'] = $1
|
35
|
+
when /^Request rate: (\d+\.\d)/ then res['req/s'] = $1
|
36
|
+
when /^Reply time .* response (\d+\.\d)/ then res['reply time'] = $1
|
37
|
+
when /^Net I\/O: (\d+\.\d)/ then res['net io (KB/s)'] = $1
|
38
|
+
when /^Errors: total (\d+)/ then res['errors'] = $1
|
39
|
+
when /^Reply rate .*min (\d+\.\d) avg (\d+\.\d) max (\d+\.\d) stddev (\d+\.\d)/ then
|
40
|
+
res['replies/s min'] = $1
|
41
|
+
res['replies/s avg'] = $2
|
42
|
+
res['replies/s max'] = $3
|
43
|
+
res['replies/s stddev'] = $4
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
return res
|
49
|
+
end
|
50
|
+
|
51
|
+
def vary_rate(uri, configuration)
|
52
|
+
puts "Config is #{configuration.inspect}" if configuration['verbose']
|
53
|
+
results = {}
|
54
|
+
report = Table(:column_names => COLUMN_NAMES)
|
55
|
+
|
56
|
+
(configuration['low_rate']..configuration['high_rate']).step(configuration['rate_step']) do |rate|
|
57
|
+
results[rate] = benchmark(configuration.merge({'httperf_rate' => rate, 'httperf_uri' => uri}))
|
58
|
+
report << results[rate].merge({'rate' => rate})
|
59
|
+
|
60
|
+
puts report.to_s
|
61
|
+
puts results[rate]['output'] if results[rate]['errors'].to_i > 0
|
62
|
+
end
|
63
|
+
|
64
|
+
report
|
65
|
+
end
|
66
|
+
|
67
|
+
def run_tests(configuration)
|
68
|
+
reports = {}
|
69
|
+
configuration['uris'].uniq.each do |uri|
|
70
|
+
reports[uri] = vary_rate(uri, configuration)
|
71
|
+
end
|
72
|
+
|
73
|
+
# TODO: Factor out to create_httperf_wlog
|
74
|
+
if !configuration['httperf_wlog'] && configuration['uris'].length > 1 && configuration['average']
|
75
|
+
replay_log = File.open('tmp_replay_log', 'w')
|
76
|
+
path = replay_log.path
|
77
|
+
puts "Tmp replay log is at #{path}" if configuration['verbose']
|
78
|
+
index = 1
|
79
|
+
configuration['uris'].each do |uri|
|
80
|
+
replay_log.print uri
|
81
|
+
replay_log.putc 0 if index < configuration['uris'].length # ASCII NUL Terminate join paths
|
82
|
+
index = index + 1
|
83
|
+
end
|
84
|
+
replay_log.close
|
85
|
+
puts "Replay log is at #{path}" if configuration['verbose']
|
86
|
+
reports["Avg"] = vary_rate('httperf_wlog' => "y,#{path}")
|
87
|
+
end
|
88
|
+
reports
|
89
|
+
end
|
90
|
+
|
91
|
+
def load_test_data(configuration)
|
92
|
+
reports = {}
|
93
|
+
configuration['uris'].each do |uri|
|
94
|
+
reports[uri] = ::Ruport::Data::Table.new(:column_names => COLUMN_NAMES)
|
95
|
+
times = [130.7, 132.7, 180.4, 438.3, 591.9, 686.9, 739.4, 661.3, 727.1, 546.5, 711.1, 893.7, 870.0]
|
96
|
+
conns = [5.0, 21.5, 28.8, 30.6, 26.3, 24.7, 23.0, 25.8, 28.0, 27.4, 27.9, 22.2, 22.7]
|
97
|
+
1.upto(10) do |i|
|
98
|
+
reports[uri] << {'rate' => i*10 - 10,
|
99
|
+
'conn/s' => conns[i % conns.length],
|
100
|
+
'reply time' => times[i % times.length]}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
reports
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
class Configuration
|
2
|
+
def initialize(opts={})
|
3
|
+
@conf = {'httperf_timeout' => 20,
|
4
|
+
'httperf_num-call' => 1,
|
5
|
+
'httperf_num-conns' => 100,
|
6
|
+
'httperf_rate' => 5,
|
7
|
+
'port' => 80,
|
8
|
+
'uri' => '/',
|
9
|
+
'low_conns' => 50,
|
10
|
+
'high_conns' => 550,
|
11
|
+
'conns_step' => 100,
|
12
|
+
'low_rate' => 5,
|
13
|
+
'high_rate' => 250,
|
14
|
+
'rate_step' => 20,
|
15
|
+
'uris' => ['/'],
|
16
|
+
'use_test_data' => false,
|
17
|
+
'batik-rasterizer-jar' => '/opt/batik/batik-rasterizer.jar',
|
18
|
+
'graph_renderer' => 'GChartRenderer',
|
19
|
+
'average' => false,
|
20
|
+
'output_dir' => './'
|
21
|
+
}.merge(opts)
|
22
|
+
|
23
|
+
# This is a little too much 'magic'
|
24
|
+
if @conf['httperf_wsesslog']
|
25
|
+
puts "Using httperf_wsesslog"
|
26
|
+
@conf.delete("httperf_num-call")
|
27
|
+
@conf.delete("httperf_num-conns")
|
28
|
+
@conf["httperf_add-header"] = "'Content-Type: application/x-www-form-urlencoded\\n'"
|
29
|
+
# TODO: Add AcceptEncoding: gzip,deflate option
|
30
|
+
end
|
31
|
+
|
32
|
+
@conf
|
33
|
+
end
|
34
|
+
|
35
|
+
def [](prop)
|
36
|
+
@conf[prop]
|
37
|
+
end
|
38
|
+
|
39
|
+
def []=(prop, value)
|
40
|
+
@conf[prop] = value
|
41
|
+
end
|
42
|
+
|
43
|
+
def merge(other_hash)
|
44
|
+
@conf.dup.merge(other_hash)
|
45
|
+
end
|
46
|
+
|
47
|
+
def inspect
|
48
|
+
@conf.inspect
|
49
|
+
end
|
50
|
+
|
51
|
+
def pretty_print
|
52
|
+
io = StringIO.new
|
53
|
+
io.puts
|
54
|
+
io.puts "Using these parameters:"
|
55
|
+
@conf.sort.each{ |k, v| io.puts " #{k}=#{v}"}
|
56
|
+
io.puts
|
57
|
+
io.read
|
58
|
+
end
|
59
|
+
|
60
|
+
def graph_renderer_class
|
61
|
+
Object.const_get(@conf['graph_renderer'].to_s)
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
class BaseRenderer
|
2
|
+
attr :title, true
|
3
|
+
attr :width, true
|
4
|
+
attr :height, true
|
5
|
+
attr :path, true
|
6
|
+
attr :series
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@series = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_series(new_series)
|
13
|
+
series.push(new_series)
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_html
|
17
|
+
return "DATA"
|
18
|
+
end
|
19
|
+
|
20
|
+
def find_max_y_value
|
21
|
+
series.map{|s| s.y_values.max}.max
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.generate_graphs(reports, configuration)
|
25
|
+
graphs = {}
|
26
|
+
reports.each do |uri, report|
|
27
|
+
graphs[uri] = []
|
28
|
+
|
29
|
+
puts "For '#{uri}' the values are #{report.column('reply time').join(', ')}" if configuration['verbose']
|
30
|
+
request_rate_graph = configuration.graph_renderer_class.new
|
31
|
+
request_rate_graph.title = "Demanded vs. Achieved Request Rate (r/s)"
|
32
|
+
request_rate_graph.path = uri
|
33
|
+
request_rate_graph.width = 600
|
34
|
+
request_rate_graph.height = 300
|
35
|
+
|
36
|
+
if reports['Avg']
|
37
|
+
avg_request_rate = GraphSeries.new(:area, report.column('rate'), reports['Avg'].column('conn/s').map{|x| x.to_f}, "Avg")
|
38
|
+
request_rate_graph.add_series(avg_request_rate)
|
39
|
+
end
|
40
|
+
|
41
|
+
request_rate = GraphSeries.new(:line, report.column('rate'), report.column('conn/s').map{|x| x.to_f}, "Requests for '#{uri}'")
|
42
|
+
request_rate_graph.add_series(request_rate)
|
43
|
+
|
44
|
+
graphs[uri] << request_rate_graph.to_html
|
45
|
+
|
46
|
+
response_time_graph = configuration.graph_renderer_class.new
|
47
|
+
response_time_graph.path = uri
|
48
|
+
response_time_graph.title = "Demanded Request Rate (r/s) vs. Response Time"
|
49
|
+
response_time_graph.width = 600
|
50
|
+
response_time_graph.height = 300
|
51
|
+
|
52
|
+
if reports['Avg']
|
53
|
+
avg_response_time = GraphSeries.new(:area, report.column('rate'), reports['Avg'].column('reply time').map{|x| x.to_f}, "Avg")
|
54
|
+
response_time_graph.add_series(avg_response_time)
|
55
|
+
end
|
56
|
+
|
57
|
+
response_time = GraphSeries.new(:line, report.column('rate'), report.column('reply time'), "Requests for '#{uri}'")
|
58
|
+
response_time_graph.add_series(response_time)
|
59
|
+
|
60
|
+
graphs[uri] << response_time_graph.to_html
|
61
|
+
end
|
62
|
+
|
63
|
+
max_rate_graph = configuration.graph_renderer_class.new
|
64
|
+
max_rate_graph.title = "Max Achieved Connection Rate"
|
65
|
+
max_rate_graph.width = 600
|
66
|
+
max_rate_graph.height = 300
|
67
|
+
|
68
|
+
reports.keys.each do |key|
|
69
|
+
max = reports[key].column('conn/s').map{|x| x.to_i}.max.to_i
|
70
|
+
max_request_rate = GraphSeries.new(:bar, [key], [max], "Max Request Rate for '#{key}'")
|
71
|
+
max_rate_graph.add_series(max_request_rate)
|
72
|
+
end
|
73
|
+
graphs['summary_graph'] = max_rate_graph
|
74
|
+
graphs
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'gchart'
|
2
|
+
|
3
|
+
class GChartRenderer < BaseRenderer
|
4
|
+
|
5
|
+
def to_html
|
6
|
+
#"<img src='#{render_graph.to_url}'/>"
|
7
|
+
render_graph.to_url
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
def available_colors
|
12
|
+
[:red, :yellow, :green, :blue, :black]
|
13
|
+
end
|
14
|
+
|
15
|
+
def render_graph
|
16
|
+
chart_type = series[0].type.to_s
|
17
|
+
chart_type = 'line' if chart_type == 'area'
|
18
|
+
|
19
|
+
chart = GChart.send(chart_type) do |g|
|
20
|
+
|
21
|
+
x_values = series[0].x_values
|
22
|
+
y_values = series[0].y_values
|
23
|
+
|
24
|
+
x_values = series.map{|s| s.x_values} if chart_type.to_sym == :bar
|
25
|
+
|
26
|
+
series_data = series.map{|s| s.y_values}
|
27
|
+
|
28
|
+
g.data = series_data
|
29
|
+
|
30
|
+
# chg, grid lines
|
31
|
+
# chm data point makers
|
32
|
+
# chma, margins
|
33
|
+
# chm, flags :chm => "fMax,FF0000,0,#{y_values.index(y_values.max)},15"
|
34
|
+
# cht, bvg (bar vertical grouped)
|
35
|
+
# chbh, bar spacing
|
36
|
+
# chdlp, legend on bottom, vertical
|
37
|
+
|
38
|
+
# TODO: Should case off of chart types
|
39
|
+
|
40
|
+
extras = {:chg => "#{100/(y_values.size)},20,1,5",
|
41
|
+
:chm => "o,0066FF,0,-1.0,6",
|
42
|
+
:chma => "20,20,20,30|80,20",
|
43
|
+
:chdlp => "bv"}
|
44
|
+
|
45
|
+
if chart_type.to_sym == :bar
|
46
|
+
extras = extras.merge({:chm => '', :chbh => 'a,20,20'})
|
47
|
+
# chm=N*f0*,000000,1,-1,11|N*f0*,000000,2,-1,11|N*f1*,000000,3,-1,11|N*f2*,FF0000,0,0,18
|
48
|
+
g.grouped = true
|
49
|
+
end
|
50
|
+
|
51
|
+
g.extras = extras
|
52
|
+
|
53
|
+
g.axis(:bottom) do |a|
|
54
|
+
a.labels = [0] << x_values
|
55
|
+
a.text_color = :black
|
56
|
+
end
|
57
|
+
|
58
|
+
g.axis(:left) do |a|
|
59
|
+
interval = (y_values.max/10).to_i
|
60
|
+
interval = 1 if interval == 0
|
61
|
+
a.labels = (0..(y_values.max.to_i)).to_a.select{|y| y % interval == 0}
|
62
|
+
a.text_color = :black
|
63
|
+
end
|
64
|
+
|
65
|
+
if chart_type.to_sym == :bar
|
66
|
+
g.orientation = :vertical
|
67
|
+
end
|
68
|
+
|
69
|
+
colors = available_colors
|
70
|
+
g.colors = series.map{|s| colors.pop}
|
71
|
+
|
72
|
+
g.legend = series.map{|s| s.label}
|
73
|
+
|
74
|
+
g.title = title
|
75
|
+
|
76
|
+
g.width = width
|
77
|
+
g.height = height
|
78
|
+
|
79
|
+
g.entire_background = "f4f4f4"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
def rasterize_graphs
|
2
|
+
batik_location = @conf['batik-rasterizer-jar']
|
3
|
+
if File.exist?(batik_location)
|
4
|
+
`java -jar #{batik_location} *.svg > /dev/null 2>&1`
|
5
|
+
|
6
|
+
Dir.glob('*.png').each do |png|
|
7
|
+
if File.size(png) > 0
|
8
|
+
File.delete(png.sub('.png', '.svg'))
|
9
|
+
else
|
10
|
+
puts "Error rasterizing #{png}, leaving SVG intact."
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
@graphs.each do |uri, graph|
|
15
|
+
@graphs[uri].each do |file_name|
|
16
|
+
@graphs[uri] = @graphs[uri].map{|x| File.exist?(x.sub('.svg', '.png')) ? x.sub('.svg', '.png') : x}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'scruffy'
|
2
|
+
|
3
|
+
class ScruffyRenderer < BaseRenderer
|
4
|
+
|
5
|
+
def to_html
|
6
|
+
render_graph
|
7
|
+
"<img src='#{@file_name}'/>"
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
def format_file_name(desired_name)
|
12
|
+
desired_name.gsub("/","_").gsub("=","_").gsub("?","_")
|
13
|
+
end
|
14
|
+
|
15
|
+
def render_graph
|
16
|
+
graph = Scruffy::Graph.new
|
17
|
+
graph.title = title
|
18
|
+
graph.renderer = Scruffy::Renderers::Standard.new(:values => series[0].x_values.size + 1)
|
19
|
+
graph.point_markers = series[0].x_values
|
20
|
+
|
21
|
+
series.each do |s|
|
22
|
+
graph.add s.type.to_sym, s.label, s.y_values
|
23
|
+
end
|
24
|
+
|
25
|
+
@file_name = format_file_name("request_graph_#{path}.svg")
|
26
|
+
|
27
|
+
graph.render :to => @file_name, :min_value => 0, :max_value => find_max_y_value
|
28
|
+
|
29
|
+
graph
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class GraphSeries
|
2
|
+
attr :x_values, true
|
3
|
+
attr :y_values, true
|
4
|
+
attr :label, true
|
5
|
+
attr :type, true
|
6
|
+
attr :path, true
|
7
|
+
|
8
|
+
def initialize(t, xs, ys, l, p=nil)
|
9
|
+
@type = t
|
10
|
+
@x_values = xs.map{|x| x.to_f}
|
11
|
+
@y_values = ys.map{|x| x.to_f}
|
12
|
+
@label = l
|
13
|
+
@path = p
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class HtmlReport
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
def initialize(reports, graphs, configuration)
|
5
|
+
date = Time.now
|
6
|
+
host = configuration['host']
|
7
|
+
title = "Report for #{host}"
|
8
|
+
uris = configuration['uris']
|
9
|
+
command_run = configuration["command_run"]
|
10
|
+
notes = configuration["notes"]
|
11
|
+
summary_graph = graphs['summary_graph']
|
12
|
+
|
13
|
+
output_file = HtmlReport.determine_output_file(configuration['output_file'], configuration['output_dir'])
|
14
|
+
|
15
|
+
template = File.read(File.dirname(__FILE__) + '/report.html.erb')
|
16
|
+
result = ERB.new(template).result(binding).to_s
|
17
|
+
|
18
|
+
File.open(output_file, "w") do |file|
|
19
|
+
file.puts result
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.determine_output_file(output_file, output_dir)
|
24
|
+
return output_file if output_file
|
25
|
+
file = File.join(output_dir, "load_test.html")
|
26
|
+
i = 0
|
27
|
+
while File.exist?(file) do
|
28
|
+
i = i + 1
|
29
|
+
file = File.join(output_dir, "load_test_#{i}.html")
|
30
|
+
end
|
31
|
+
file
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
2
|
+
"http://www.w3.org/TR/html4/strict.dtd">
|
3
|
+
<html lang="en">
|
4
|
+
<head>
|
5
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
6
|
+
<title><%= title %></title>
|
7
|
+
<style type="text/css">
|
8
|
+
body,
|
9
|
+
html {
|
10
|
+
margin:0;
|
11
|
+
padding:0;
|
12
|
+
color:#000;
|
13
|
+
}
|
14
|
+
body {
|
15
|
+
min-width:950px;
|
16
|
+
}
|
17
|
+
#wrap {
|
18
|
+
margin:0 auto;
|
19
|
+
width:950px;
|
20
|
+
}
|
21
|
+
#header h1 {
|
22
|
+
padding:5px;
|
23
|
+
margin:0;
|
24
|
+
text-align: center;
|
25
|
+
}
|
26
|
+
#header h3 {
|
27
|
+
margin: 0;
|
28
|
+
}
|
29
|
+
#nav {
|
30
|
+
|
31
|
+
padding:5px;
|
32
|
+
}
|
33
|
+
#nav ul{
|
34
|
+
margin:0;
|
35
|
+
padding:0;
|
36
|
+
list-style:none;
|
37
|
+
}
|
38
|
+
#nav li{
|
39
|
+
display:inline;
|
40
|
+
margin:0;
|
41
|
+
padding:0;
|
42
|
+
}
|
43
|
+
#main {
|
44
|
+
float:right;
|
45
|
+
width:700px;
|
46
|
+
}
|
47
|
+
#main h2, #main h3, #main p {
|
48
|
+
padding:0 10px;
|
49
|
+
}
|
50
|
+
#sidebar {
|
51
|
+
float:left;
|
52
|
+
width:240px;
|
53
|
+
}
|
54
|
+
#sidebar ul {
|
55
|
+
margin-bottom:0;
|
56
|
+
}
|
57
|
+
#sidebar h3, #sidebar p {
|
58
|
+
padding:0 10px 0 0;
|
59
|
+
}
|
60
|
+
#footer {
|
61
|
+
clear:both;
|
62
|
+
}
|
63
|
+
#footer p {
|
64
|
+
padding:5px;
|
65
|
+
margin:0;
|
66
|
+
}
|
67
|
+
|
68
|
+
div.report {
|
69
|
+
background: #FFFFFF;
|
70
|
+
}
|
71
|
+
</style>
|
72
|
+
<script type="text/javascript"``>
|
73
|
+
function hide_reports() {
|
74
|
+
var reports = document.getElementsByClassName('report');
|
75
|
+
for (var i=0; i < reports.length; i++) {
|
76
|
+
reports[i].style.display = 'none';
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
function show_report(uri) {
|
81
|
+
hide_reports();
|
82
|
+
document.getElementById(uri).style.display = '';
|
83
|
+
}
|
84
|
+
|
85
|
+
function uri_change_handler(uri) {
|
86
|
+
var page_select = document.getElementById('pages');
|
87
|
+
show_report(page_select.value);
|
88
|
+
}
|
89
|
+
</script>
|
90
|
+
|
91
|
+
</head>
|
92
|
+
<body>
|
93
|
+
<div id="wrap">
|
94
|
+
<div id="header">
|
95
|
+
<h1>Load testing report</h1>
|
96
|
+
<h3>Date: <%= date %></h3>
|
97
|
+
<h3>Host: <%= host %></h3>
|
98
|
+
<h3>Page:
|
99
|
+
<select id="pages" onchange="uri_change_handler()">
|
100
|
+
<option value='overview'>Overview</option>
|
101
|
+
<% uris.each do |uri|%>
|
102
|
+
<option value='page_<%= uri %>'><%=uri%></option>
|
103
|
+
<% end %>
|
104
|
+
</select>
|
105
|
+
</h3>
|
106
|
+
|
107
|
+
</div>
|
108
|
+
<div id="nav">
|
109
|
+
|
110
|
+
</div>
|
111
|
+
<div id="sidebar">
|
112
|
+
<div style="display:none">
|
113
|
+
<h3>Tested Pages</h3>
|
114
|
+
<ul>
|
115
|
+
<li><a href="#" onclick="show_report('overview');" title="Overview">Overview</a></li>
|
116
|
+
<% uris.each do |uri|%>
|
117
|
+
<li><a href="#" onclick="show_report('page_<%= uri %>');" title="<%= uri %>"><%= uri %></a></li>
|
118
|
+
<% end %>
|
119
|
+
</ul>
|
120
|
+
</div>
|
121
|
+
<% if notes %>
|
122
|
+
<h3>Report Notes</h3>
|
123
|
+
<p><%= notes %></p>
|
124
|
+
<% end %>
|
125
|
+
<h3>Info</h3>
|
126
|
+
<ul>
|
127
|
+
<li><a href="#" onclick="show_report('graph_discussion');" title="Discussion">Discussion</a></li>
|
128
|
+
</ul>
|
129
|
+
|
130
|
+
</div>
|
131
|
+
<div id="main">
|
132
|
+
<h2>Report Detail</h2>
|
133
|
+
|
134
|
+
<div id="overview" class='report'>
|
135
|
+
<img src="<%= summary_graph.to_html %>"/>
|
136
|
+
</div>
|
137
|
+
|
138
|
+
<div id="graph_discussion" class='report' style="display:none;">
|
139
|
+
Discussion on what these graphs mean.
|
140
|
+
|
141
|
+
• Increase concurrent connections, and see at which point your app will no longer serve at the demanded request rate
|
142
|
+
• Find which pages are heavier/lighter than others
|
143
|
+
• Replay a log (with a little work)
|
144
|
+
|
145
|
+
• Actually hits your app with load, doesn't just extrapolate out from a few data points
|
146
|
+
• Start broad and zero in on connection limits
|
147
|
+
• Run from a server, not your laptop
|
148
|
+
</div>
|
149
|
+
|
150
|
+
<% reports.each do |uri, report| %>
|
151
|
+
<div id="page_<%= uri %>" class='report' style="display:none;">
|
152
|
+
<p>Report for <%= "http://#{host}#{uri}" %></p>
|
153
|
+
<% graphs[uri].each do |graph| %>
|
154
|
+
<img src="<%= graph %>"/>
|
155
|
+
<% end %>
|
156
|
+
<pre>
|
157
|
+
<%= report.to_s %>
|
158
|
+
</pre>
|
159
|
+
</div>
|
160
|
+
<% end %>
|
161
|
+
</div>
|
162
|
+
<div id="footer" align="center">
|
163
|
+
<p>Generated at <%= date %></p>
|
164
|
+
<div>
|
165
|
+
<p><a onclick="document.getElementById('command_container').style.display = 'block';">Show Command<a/></p>
|
166
|
+
<div id="command_container" style="display:none">
|
167
|
+
<pre><%= command_run %></pre>
|
168
|
+
</div>
|
169
|
+
</div>
|
170
|
+
</div>
|
171
|
+
</div>
|
172
|
+
</body>
|
173
|
+
</html>
|
174
|
+
|
data/script/console
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# File: script/console
|
3
|
+
irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
|
4
|
+
|
5
|
+
libs = " -r irb/completion"
|
6
|
+
# Perhaps use a console_lib to store any extra methods I may want available in the cosole
|
7
|
+
# libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
|
8
|
+
libs << " -r #{File.dirname(__FILE__) + '/../lib/autograph.rb'}"
|
9
|
+
puts "Loading autograph gem"
|
10
|
+
exec "#{irb} #{libs} --simple-prompt"
|
data/script/destroy
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/destroy'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
14
|
+
RubiGen::Scripts::Destroy.new.run(ARGV)
|
data/script/generate
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/generate'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
14
|
+
RubiGen::Scripts::Generate.new.run(ARGV)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
2
|
+
|
3
|
+
class TestAutograph < Test::Unit::TestCase
|
4
|
+
require 'mocha'
|
5
|
+
|
6
|
+
def setup
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_truth
|
10
|
+
assert(true)
|
11
|
+
end
|
12
|
+
|
13
|
+
# def test_uniq_uris
|
14
|
+
# runner = AutoPerf.new(:host => 'example.com')
|
15
|
+
# AutoPerf.any_instance.expects(:generate_graphs)
|
16
|
+
# runner.expects(:generate_graphs)
|
17
|
+
# end
|
18
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
2
|
+
|
3
|
+
class TestHtmlReport < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def test_takes_a_hash
|
6
|
+
c = Configuration.new(:a => 1)
|
7
|
+
assert_equal c[:a], 1
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_has_defaults
|
11
|
+
c = Configuration.new()
|
12
|
+
assert_equal c['low_rate'], 5
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_can_override_defaults
|
16
|
+
c = Configuration.new('low_rate' => 1)
|
17
|
+
assert_equal c['low_rate'], 1
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_graph_render_klazz_helper
|
21
|
+
c = Configuration.new()
|
22
|
+
assert_equal c.graph_renderer_class, GChartRenderer
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_pretty_print_returns_a_string
|
26
|
+
c = Configuration.new().pretty_print
|
27
|
+
assert_equal c.class, String
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
2
|
+
|
3
|
+
class TestGraphSeries < Test::Unit::TestCase
|
4
|
+
require 'mocha'
|
5
|
+
require 'autograph'
|
6
|
+
|
7
|
+
def setup
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_constructor
|
11
|
+
x_values = [1,2,3]
|
12
|
+
y_values = [4,5,6]
|
13
|
+
|
14
|
+
gs = GraphSeries.new(:type, x_values, y_values, 'label')
|
15
|
+
assert_equal(gs.type, :type)
|
16
|
+
assert_equal(gs.x_values, x_values)
|
17
|
+
assert_equal(gs.y_values, y_values)
|
18
|
+
assert_equal(gs.label, 'label')
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_constructor_converts_to_floats
|
22
|
+
x_values = [1,2,3]
|
23
|
+
y_values = [4,5,6]
|
24
|
+
|
25
|
+
gs = GraphSeries.new(:type, x_values, y_values, 'label')
|
26
|
+
gs.y_values.each{|y| assert_equal(y.class, Float) }
|
27
|
+
gs.x_values.each{|x| assert_equal(x.class, Float) }
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
2
|
+
|
3
|
+
class TestHtmlReport < Test::Unit::TestCase
|
4
|
+
require 'mocha'
|
5
|
+
require 'autograph'
|
6
|
+
|
7
|
+
def setup
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_requires_params
|
11
|
+
error = nil
|
12
|
+
begin
|
13
|
+
HtmlReport.new({})
|
14
|
+
rescue => e
|
15
|
+
error = e
|
16
|
+
end
|
17
|
+
assert(!e.nil?)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_uses_defined_output_file
|
21
|
+
output_file = HtmlReport.determine_output_file('file.txt', nil)
|
22
|
+
assert_equal(output_file, 'file.txt')
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_uses_defined_output_dir
|
26
|
+
output_file = HtmlReport.determine_output_file(nil, '/opt')
|
27
|
+
assert_equal(output_file, '/opt/load_test.html')
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_generate_output_file
|
31
|
+
File.expects(:exist?).with('./load_test.html').returns(true)
|
32
|
+
File.expects(:exist?).with('./load_test_1.html').returns(true)
|
33
|
+
File.expects(:exist?).with('./load_test_2.html').returns(false)
|
34
|
+
output_file = HtmlReport.determine_output_file(nil, '.')
|
35
|
+
assert_equal(output_file, './load_test_2.html')
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: autograph
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 2
|
9
|
+
version: 0.1.2
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Nick Stielau
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-05-20 00:00:00 -07:00
|
18
|
+
default_executable: autograph
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: builder
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 2
|
29
|
+
- 1
|
30
|
+
- 2
|
31
|
+
version: 2.1.2
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: ruport
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 1
|
43
|
+
- 6
|
44
|
+
- 3
|
45
|
+
version: 1.6.3
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: scruffy
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 0
|
57
|
+
- 2
|
58
|
+
- 6
|
59
|
+
version: 0.2.6
|
60
|
+
type: :runtime
|
61
|
+
version_requirements: *id003
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: gchart
|
64
|
+
prerelease: false
|
65
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
segments:
|
70
|
+
- 1
|
71
|
+
- 0
|
72
|
+
- 0
|
73
|
+
version: 1.0.0
|
74
|
+
type: :runtime
|
75
|
+
version_requirements: *id004
|
76
|
+
description: Autograph wraps httperf, running multiple tests while varying the parameters, graphing the output.
|
77
|
+
email: nick.stielau@gmail.com
|
78
|
+
executables:
|
79
|
+
- autograph
|
80
|
+
extensions: []
|
81
|
+
|
82
|
+
extra_rdoc_files:
|
83
|
+
- README.rdoc
|
84
|
+
- TODO
|
85
|
+
files:
|
86
|
+
- .gitignore
|
87
|
+
- History.txt
|
88
|
+
- README.rdoc
|
89
|
+
- Rakefile
|
90
|
+
- TODO
|
91
|
+
- VERSION
|
92
|
+
- bin/autograph
|
93
|
+
- lib/autograph.rb
|
94
|
+
- lib/autograph/autoperf.rb
|
95
|
+
- lib/autograph/configuration.rb
|
96
|
+
- lib/autograph/graph_renderers/base_renderer.rb
|
97
|
+
- lib/autograph/graph_renderers/gchart_renderer.rb
|
98
|
+
- lib/autograph/graph_renderers/rasterized_scruffy_renderer.rb
|
99
|
+
- lib/autograph/graph_renderers/scruffy_renderer.rb
|
100
|
+
- lib/autograph/graph_series.rb
|
101
|
+
- lib/autograph/html_report.rb
|
102
|
+
- lib/autograph/report.html.erb
|
103
|
+
- load_test_report_example.html
|
104
|
+
- script/console
|
105
|
+
- script/destroy
|
106
|
+
- script/generate
|
107
|
+
- test/test_autograph.rb
|
108
|
+
- test/test_configuration.rb
|
109
|
+
- test/test_graph_series.rb
|
110
|
+
- test/test_helper.rb
|
111
|
+
- test/test_html_report.rb
|
112
|
+
has_rdoc: true
|
113
|
+
homepage: http://github.com/nstielau/autograph
|
114
|
+
licenses: []
|
115
|
+
|
116
|
+
post_install_message:
|
117
|
+
rdoc_options:
|
118
|
+
- --charset=UTF-8
|
119
|
+
require_paths:
|
120
|
+
- lib
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
segments:
|
126
|
+
- 0
|
127
|
+
version: "0"
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
segments:
|
133
|
+
- 0
|
134
|
+
version: "0"
|
135
|
+
requirements: []
|
136
|
+
|
137
|
+
rubyforge_project:
|
138
|
+
rubygems_version: 1.3.6
|
139
|
+
signing_key:
|
140
|
+
specification_version: 3
|
141
|
+
summary: Simple httperf runner with nice html reports with graphs.
|
142
|
+
test_files:
|
143
|
+
- test/test_autograph.rb
|
144
|
+
- test/test_configuration.rb
|
145
|
+
- test/test_graph_series.rb
|
146
|
+
- test/test_helper.rb
|
147
|
+
- test/test_html_report.rb
|