hansel 0.1.7 → 0.2.0

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/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