hansel 0.1.7 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # hansel
2
+
3
+ Hansel is a pure ruby driver for httperf for automated load and performance testing. It will load
4
+ a job queue file, in a yaml format, run httperf with each job
5
+
6
+ # Installing Httperf and Hansel
7
+
8
+ ## Httperf
9
+
10
+ For Linux (Ubuntu):
11
+
12
+ apt-get update && apt-get -y install rubygems httperf
13
+
14
+ On MacOS X using homebrew:
15
+
16
+ brew install httperf
17
+
18
+ finally:
19
+
20
+ gem install hansel
21
+
22
+ ## Optional -- Installing Octave using macports (warning: long!)
23
+
24
+ sudo port install octave
25
+
26
+ # Example usage
27
+
28
+ Create a job queue file in ~/.hansel/jobs.yml:
29
+
30
+ ---
31
+ - :server: www.example.com
32
+ :uri: /
33
+ :num_conns: 50
34
+ :low_rate: 10
35
+ :high_rate: 50
36
+ :rate_step: 10
37
+
38
+ - :server: www.apple.com
39
+ :uri: /
40
+ :num_conns: 50
41
+ :low_rate: 10
42
+ :high_rate: 50
43
+ :rate_step: 10
44
+
45
+ and run Hansel
46
+
47
+ hansel --verbose --format=octave
48
+
49
+ By default, the output is written into the ~/hansel_output directory. When the octave format is
50
+ specified, it uses the default template octave.m.erb in the project templates. Here is a sample
51
+ output from the previous job:
52
+
53
+ rate = [5, 10, 15, 20];
54
+ request_rate = [5.1, 9.3, 12.9, 15.8];
55
+ connection_rate = [5.1, 9.3, 12.9, 15.8];
56
+ reply_rate_avg = [0.0, 0.0, 0.0, 0.0];
57
+ reply_rate_max = [0.0, 0.0, 0.0, 0.0];
58
+ reply_time = [88.4, 93.4, 89.2, 89.1];
59
+ reply_rate_stddev = [0.0, 0.0, 0.0, 0.0];
60
+ errors = [0, 0, 0, 0];
61
+
62
+ plot(rate, request_rate, '-k*');
63
+ hold on;
64
+ plot(rate, connection_rate, '-kd');
65
+ hold on;
66
+ plot(rate, reply_rate_max, '-kp');
67
+ hold on;
68
+ plot(rate, reply_rate_max, '-k+');
69
+ hold on;
70
+ plot(rate, reply_rate_stddev, '-kh');
71
+ hold on;
72
+ plot(rate, reply_time, '-g*');
73
+ hold on;
74
+ plot(rate, errors, '-r*');
75
+
76
+ grid on;
77
+
78
+ axis([0 20 0 20]);
79
+ title('Hansel report for www.example.com:80/ (10 connections per run)')
80
+ xlabel('Demanded Request Rate');
81
+ legend('Request Rate', 'Connection Rate', 'Avg. reply rate', 'Max. reply rate', 'Reply rate StdDev', 'Reply time', 'Errors');
82
+ print('/Users/paulm/hansel_output/hansel-10.m.png', '-dpng')
83
+
84
+ Run octave on it will produce graph as a png file.
85
+
86
+ ## Note on Patches/Pull Requests
87
+
88
+ * Fork the project.
89
+ * Make your feature addition or bug fix.
90
+ * Add tests for it. This is important so I don't break it in a
91
+ future version unintentionally.
92
+ * Commit, do not mess with rakefile, version, or history.
93
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
94
+ * Send me a pull request. Bonus points for topic branches.
95
+
96
+ ## References
97
+
98
+ "HTTP Perforance Testing with httperf":http://agiletesting.blogspot.com/2005/04/http-performance-testing-with-httperf.html
99
+ "Perforance vs Load Testing":http://agiletesting.blogspot.com/2005/04/more-on-performance-vs-load-testing.html
100
+
101
+ ## Copyright
102
+
103
+ Copyright (c) 2010 Paul Mylchreest. See LICENSE for details.
data/Rakefile CHANGED
@@ -1,6 +1,11 @@
1
1
  require 'rubygems'
2
2
  require 'rake'
3
- require "spec/rake/spectask"
3
+
4
+ begin
5
+ require "spec/rake/spectask"
6
+ rescue LoadError
7
+ puts "rspec (or a dependency) not available. Install it with: gem install rspec"
8
+ end
4
9
 
5
10
  task :default => :spec
6
11
 
@@ -19,11 +24,14 @@ begin
19
24
  require 'jeweler'
20
25
  Jeweler::Tasks.new do |gem|
21
26
  gem.name = "hansel"
27
+ gem.executables = "hansel"
22
28
  gem.summary = %Q{Ruby driver for httperf - automated load and performance testing}
23
29
  gem.description = %Q{Ruby driver for httperf - automated load and performance testing}
24
30
  gem.email = "paul.mylchreest@mac.com"
25
31
  gem.homepage = "http://github.com/xlymian/hansel"
26
32
  gem.authors = ["Paul Mylchreest"]
33
+ gem.add_dependency('rspec')
34
+ gem.add_dependency 'typhoeus'
27
35
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
28
36
  end
29
37
  Jeweler::GemcutterTasks.new
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.7
1
+ 0.2.0
data/bin/hansel CHANGED
@@ -10,10 +10,7 @@ class File
10
10
  end
11
11
  end
12
12
 
13
- require File.here '/../lib/arg_parser'
14
- require File.here '/../lib/config'
15
13
  require File.here '/../lib/hansel'
16
-
17
- options = Hansel::Config.new(ARGV).options
14
+ options = HanselCore::ArgParser.new( ARGV ).parse
18
15
  exit if options.exit
19
- Hansel::Hansel.new(options).run
16
+ HanselCore::Hansel.new( options ).load_job_queue.run
data/hansel.gemspec CHANGED
@@ -5,38 +5,43 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{hansel}
8
- s.version = "0.1.7"
8
+ s.version = "0.2.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Paul Mylchreest"]
12
- s.date = %q{2010-05-08}
12
+ s.date = %q{2010-06-13}
13
13
  s.default_executable = %q{hansel}
14
14
  s.description = %q{Ruby driver for httperf - automated load and performance testing}
15
15
  s.email = %q{paul.mylchreest@mac.com}
16
16
  s.executables = ["hansel"]
17
17
  s.extra_rdoc_files = [
18
18
  "LICENSE",
19
- "README.rdoc"
19
+ "README.md"
20
20
  ]
21
21
  s.files = [
22
22
  ".document",
23
23
  ".gitignore",
24
24
  "LICENSE",
25
- "README.rdoc",
25
+ "README.md",
26
26
  "Rakefile",
27
27
  "VERSION",
28
28
  "bin/hansel",
29
29
  "hansel.gemspec",
30
- "lib/arg_parser.rb",
31
- "lib/config.rb",
32
- "lib/csv_formatter.rb",
33
30
  "lib/hansel.rb",
34
- "lib/httperf_result.rb",
35
- "lib/httperf_result_parser.rb",
36
- "lib/octave_formatter.rb",
37
- "lib/yaml_formatter.rb",
31
+ "lib/hansel/arg_parser.rb",
32
+ "lib/hansel/formatting/csv_formatter.rb",
33
+ "lib/hansel/formatting/formatting.rb",
34
+ "lib/hansel/formatting/octave_formatter.rb",
35
+ "lib/hansel/formatting/yaml_formatter.rb",
36
+ "lib/hansel/hansel.rb",
37
+ "lib/hansel/httperf/httperf.rb",
38
+ "lib/hansel/httperf_result.rb",
39
+ "lib/hansel/httperf_result_parser.rb",
40
+ "lib/hansel/job_queue/job_queue.rb",
38
41
  "spec/arg_parser_spec.rb",
42
+ "spec/hansel_spec.rb",
39
43
  "spec/httperf_result_parser_spec.rb",
44
+ "spec/jobs.yml",
40
45
  "spec/spec_helper.rb",
41
46
  "templates/octave.m.erb"
42
47
  ]
@@ -47,6 +52,7 @@ Gem::Specification.new do |s|
47
52
  s.summary = %q{Ruby driver for httperf - automated load and performance testing}
48
53
  s.test_files = [
49
54
  "spec/arg_parser_spec.rb",
55
+ "spec/hansel_spec.rb",
50
56
  "spec/httperf_result_parser_spec.rb",
51
57
  "spec/spec_helper.rb"
52
58
  ]
@@ -56,9 +62,15 @@ Gem::Specification.new do |s|
56
62
  s.specification_version = 3
57
63
 
58
64
  if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
65
+ s.add_runtime_dependency(%q<rspec>, [">= 0"])
66
+ s.add_runtime_dependency(%q<typhoeus>, [">= 0"])
59
67
  else
68
+ s.add_dependency(%q<rspec>, [">= 0"])
69
+ s.add_dependency(%q<typhoeus>, [">= 0"])
60
70
  end
61
71
  else
72
+ s.add_dependency(%q<rspec>, [">= 0"])
73
+ s.add_dependency(%q<typhoeus>, [">= 0"])
62
74
  end
63
75
  end
64
76
 
@@ -0,0 +1,84 @@
1
+ module HanselCore
2
+ #
3
+ # Parse the command configuration file options and command line arguments.
4
+ # Command line arguments override those from the configuration file
5
+ # See http://www.ruby-doc.org/stdlib/libdoc/optparse/rdoc/classes/OptionParser.html
6
+ #
7
+ class ArgParser
8
+ #
9
+ # Setup default values for the parsed options
10
+ #
11
+ def initialize args
12
+ @args = args
13
+ @options = OpenStruct.new(
14
+ :verbose => false,
15
+ :output_format => :yaml,
16
+ :output_dir => File.join(ENV['HOME'], 'hansel_output'),
17
+ :template_path => 'templates',
18
+ :template => nil,
19
+ :exit => false,
20
+ :master => nil
21
+ )
22
+ end
23
+
24
+ #
25
+ # Uses OptionParser to return an OpenStruct object describing the options.
26
+ #
27
+ def parse
28
+ # The options specified on the command line will be collected in *options*.
29
+ # We set default values here.
30
+ OptionParser.new { |options| parse_options options}.parse!(@args)
31
+ @options
32
+ end
33
+
34
+ private
35
+
36
+ def output_options options
37
+ options.on("-f", "--format=format",
38
+ "Specify the ouptut format for the results.") do |format|
39
+ @options.format = format
40
+ end
41
+ options.on("-d", "--dir=output directory",
42
+ "Specify the ouptut directory for the results.") do |format|
43
+ @options.output_dir = format
44
+ end
45
+ options.on("-t", "--template=template",
46
+ "Specify the ouptut erb template.") do |template|
47
+ @options.template = template
48
+ end
49
+ end
50
+
51
+ def other_options options
52
+ options.on("-v", "--[no-]verbose", "Run verbosely") do |verbose|
53
+ @options.verbose = verbose
54
+ end
55
+
56
+ options.on_tail("-h", "--help", "Show this message") do
57
+ puts options
58
+ @options.exit = true
59
+ end
60
+ end
61
+
62
+ def distributed_mode_options options
63
+ options.on("-m", "--master=hostname[:port]",
64
+ "Specify the master command and control machine.") do |master|
65
+ @options.master = master
66
+ end
67
+ end
68
+
69
+ def version options
70
+ options.on_tail("--version", "Show version") do
71
+ puts "Hansel version #{IO.foreach('VERSION').first.strip}"
72
+ @options.exit = true
73
+ end
74
+ end
75
+
76
+ def parse_options options
77
+ options.banner = "Usage: hansel [options]"
78
+ options.separator "Options:"
79
+ %w( output_options other_options version distributed_mode_options ).map(&:to_sym).each do |method|
80
+ self.send method, options
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1,31 +1,26 @@
1
1
  require 'csv'
2
2
 
3
- module Hansel
3
+ module HanselCore
4
4
 
5
5
  # Output to csv format
6
6
  #
7
7
  class CsvFormatter
8
- COLUMNS = %w(rate replies connection_rate request_rate reply_time net_io
9
- errors status reply_rate_min reply_rate_avg reply_rate_max
10
- reply_rate_stddev)
8
+ COLUMNS = %w(server port num_conns rate replies connection_rate request_rate reply_time net_io
9
+ errors status reply_rate_min reply_rate_avg reply_rate_max reply_rate_stddev)
11
10
 
12
- def initialize(data)
13
- @data = data
14
- @csv = ""
15
- end
16
-
17
- def line text
11
+ def self.line text
18
12
  @csv << text
19
13
  @csv << "\n"
20
14
  end
21
15
 
22
- def format_line data
16
+ def self.format_line data
23
17
  line CSV.generate_line( COLUMNS.map { |column| data.send column.to_sym } )
24
18
  end
25
19
 
26
- def format
20
+ def self.format results
21
+ @csv = ""
27
22
  line CSV.generate_line(COLUMNS)
28
- @data.each { |data| format_line data } unless @data.empty?
23
+ results.each { |data| format_line data } unless results.empty?
29
24
  @csv
30
25
  end
31
26
  end
@@ -0,0 +1,51 @@
1
+ module HanselCore
2
+ module Formatting
3
+ def yaml_formatter
4
+ load 'lib/hansel/formatting/yaml_formatter.rb'
5
+ File.open(output_filename, "w+") do |file|
6
+ file.puts YamlFormatter.format results
7
+ end
8
+ end
9
+
10
+ def csv_formatter
11
+ load 'lib/hansel/formatting/csv_formatter.rb'
12
+ File.open(output_filename, "w+") do |file|
13
+ file.puts CsvFormatter.format results
14
+ end
15
+ end
16
+
17
+ def octave_formatter
18
+ load 'lib/hansel/formatting/octave_formatter.rb'
19
+ num_conns = results.first.num_conns rescue nil
20
+ file_name = output_filename{ "-#{num_conns.to_s}" }
21
+ File.open(file_name, "w+") do |file|
22
+ file.puts OctaveFormatter.new(results,
23
+ {
24
+ :output_file_name => file_name,
25
+ :template => options.template ||
26
+ File.join( [ options.template_path, 'octave.m.erb' ] ),
27
+ }
28
+ ).format
29
+ end
30
+ end
31
+
32
+ def formatted_output
33
+ formatter = "#{options.format}_formatter".to_sym
34
+ unless self.respond_to? formatter
35
+ status "Using default octave_formatter"
36
+ octave_formatter
37
+ else
38
+ status "Using #{formatter}"
39
+ self.send formatter
40
+ end
41
+ end
42
+
43
+ def output_filename
44
+ part = block_given? ? yield : ''
45
+ type = { :yaml => 'yml', :csv => 'csv', :octave => 'm' }[options.format.to_sym]
46
+ server = results.first.server
47
+ port = results.first.port
48
+ [File.join([options.output_dir, ("#{server}:#{port}" + part)]), type].join('.')
49
+ end
50
+ end
51
+ end
@@ -1,6 +1,6 @@
1
1
  require 'erb'
2
2
 
3
- module Hansel
3
+ module HanselCore
4
4
  #
5
5
  # Output to a Octave script.
6
6
  #
@@ -0,0 +1,10 @@
1
+ module HanselCore
2
+ #
3
+ # Output to yaml format
4
+ #
5
+ class YamlFormatter
6
+ def self.format results
7
+ results.to_yaml
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,77 @@
1
+ module HanselCore
2
+ #
3
+ # Class wrapper over httperf.
4
+ #
5
+ class Hansel
6
+ include Formatting
7
+ include Httperf
8
+ include JobQueue
9
+
10
+ attr_reader :results, :options
11
+
12
+ def initialize options = OpenStruct.new {}
13
+ @options = options
14
+ @results = []
15
+ @current_rate = nil
16
+ @jobs = []
17
+ end
18
+
19
+ def config_path
20
+ @config_path ||= File.join [ENV['HOME'], '.hansel']
21
+ FileUtils.mkdir_p @config_path unless File.exists? @config_path
22
+ @config_path
23
+ end
24
+
25
+ def output_dir
26
+ @output_dir ||= File.join [ENV['HOME'], 'hansel_output']
27
+ @output_dir
28
+ end
29
+
30
+ def register
31
+ response = Typhoeus::Request.get options.master
32
+ @token = JSON.parse(response.body)['token']
33
+ end
34
+
35
+ #
36
+ # Output the results based on the requested output format
37
+ #
38
+ def output
39
+ if options.format
40
+ FileUtils.mkdir_p options.output_dir
41
+ formatted_output
42
+ end
43
+ end
44
+
45
+ def status text
46
+ puts text if options.verbose
47
+ end
48
+
49
+ #
50
+ # Run httperf from low_rate to high_rate, stepping by rate_step
51
+ #
52
+ def run
53
+ status "starting run..."
54
+ while @jobs.length > 0 do
55
+ @current_job = @jobs.pop
56
+ (@current_job.low_rate.to_i..@current_job.high_rate.to_i).step(@current_job.rate_step.to_i) do |rate|
57
+ status "running httperf at rate: #{rate}"
58
+ @current_rate = rate
59
+ httperf
60
+ end
61
+ output
62
+ @results.clear
63
+ end
64
+ status "ending run..."
65
+ end
66
+
67
+ def run_server
68
+ HanselSlaveServer.hansel = self
69
+ HanselSlaveServer.run! :host => 'localhost', :port => 4000
70
+ end
71
+
72
+ trap("INT") {
73
+ puts "Terminating."
74
+ Process.exit
75
+ }
76
+ end
77
+ end
@@ -0,0 +1,36 @@
1
+ module HanselCore
2
+ module Httperf
3
+ def build_httperf_cmd
4
+ httperf_cmd = [
5
+ "httperf --hog",
6
+ "--server=#{@current_job.server}",
7
+ "--port=#{@current_job.port}",
8
+ "--uri=#{@current_job.uri}",
9
+ "--num-conns=#{@current_job.num_conns}",
10
+ "--rate=#{@current_rate}",
11
+ @current_job.cookie && "--add-header='Cookie: #{@current_job.cookie}\\n'"
12
+ ].compact.join ' '
13
+ end
14
+
15
+ #
16
+ # Runs httperf with a given request rate. Parses the output and returns
17
+ # a hash with the results.
18
+ #
19
+ def httperf
20
+ httperf_cmd = build_httperf_cmd
21
+ IO.popen("#{httperf_cmd} 2>&1") do |pipe|
22
+ status "\n#{httperf_cmd}"
23
+ @results << (httperf_result = HttperfResult.new({
24
+ :rate => @current_rate,
25
+ :server => @current_job.server,
26
+ :port => @current_job.port,
27
+ :uri => @current_job.uri,
28
+ :num_conns => @current_job.num_conns
29
+ }))
30
+ HttperfResultParser.new(pipe).parse(httperf_result)
31
+ end
32
+ self
33
+ end
34
+
35
+ end
36
+ end
@@ -1,4 +1,4 @@
1
- module Hansel
1
+ module HanselCore
2
2
  #
3
3
  # Wrapper for parsing of the httperf run result.
4
4
  #
@@ -1,4 +1,4 @@
1
- module Hansel
1
+ module HanselCore
2
2
  #
3
3
  # Parse httperf output.
4
4
  #
@@ -0,0 +1,27 @@
1
+ module HanselCore
2
+ module JobQueue
3
+ def jobs
4
+ @jobs
5
+ end
6
+
7
+ def push_job job
8
+ job.port = 80 unless job.port
9
+ @jobs.push job
10
+ end
11
+
12
+ def pop_job job
13
+ @jobs.pop
14
+ end
15
+
16
+ #
17
+ # Load jobs from queue
18
+ #
19
+ def load_job_queue
20
+ (YAML.load_file File.join(config_path, 'jobs.yml')).map do |job|
21
+ self.push_job(OpenStruct.new job)
22
+ end
23
+ self
24
+ end
25
+
26
+ end
27
+ end