From 2f2919c156fbda2d7dfacd982b8a6e95963d9edf Mon Sep 17 00:00:00 2001 From: Nick Gauthier Date: Thu, 25 Mar 2010 12:15:01 -0400 Subject: [PATCH] added even listeners, and replaced output and reporting system with listeners --- hydra.gemspec | 5 +- lib/hydra.rb | 4 ++ lib/hydra/listener/abstract.rb | 30 +++++++++ lib/hydra/listener/minimal_output.rb | 20 ++++++ lib/hydra/listener/report_generator.rb | 25 ++++++++ lib/hydra/master.rb | 85 +++++++++----------------- lib/hydra/tasks.rb | 13 ++-- test/master_test.rb | 18 ++---- 8 files changed, 123 insertions(+), 77 deletions(-) create mode 100644 lib/hydra/listener/abstract.rb create mode 100644 lib/hydra/listener/minimal_output.rb create mode 100644 lib/hydra/listener/report_generator.rb diff --git a/hydra.gemspec b/hydra.gemspec index 7750356..64f641a 100644 --- a/hydra.gemspec +++ b/hydra.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Nick Gauthier"] - s.date = %q{2010-02-18} + s.date = %q{2010-03-25} s.description = %q{Spread your tests over multiple machines to test your code faster.} s.email = %q{nick@smartlogicsolutions.com} s.extra_rdoc_files = [ @@ -30,6 +30,9 @@ Gem::Specification.new do |s| "hydra_gray.png", "lib/hydra.rb", "lib/hydra/hash.rb", + "lib/hydra/listener/abstract.rb", + "lib/hydra/listener/minimal_output.rb", + "lib/hydra/listener/report_generator.rb", "lib/hydra/master.rb", "lib/hydra/message.rb", "lib/hydra/message/master_messages.rb", diff --git a/lib/hydra.rb b/lib/hydra.rb index d659346..f7179de 100644 --- a/lib/hydra.rb +++ b/lib/hydra.rb @@ -7,4 +7,8 @@ require 'hydra/safe_fork' require 'hydra/runner' require 'hydra/worker' require 'hydra/master' +require 'hydra/listener/abstract' +require 'hydra/listener/minimal_output' +require 'hydra/listener/report_generator' + diff --git a/lib/hydra/listener/abstract.rb b/lib/hydra/listener/abstract.rb new file mode 100644 index 0000000..9fc87ed --- /dev/null +++ b/lib/hydra/listener/abstract.rb @@ -0,0 +1,30 @@ +module Hydra #:nodoc: + module Listener #:nodoc: + # Abstract listener that implements all the events + # but does nothing. + class Abstract + # Create a new listener. + # + # Output: The IO object for outputting any information. + # Defaults to STDOUT, but you could pass a file in, or STDERR + def initialize(output = $stdout) + @output = output + end + # Fired when testing has started + def testing_begin(files) + end + + # Fired when testing finishes + def testing_end + end + + # Fired when a file is started + def file_begin(file) + end + + # Fired when a file is finished + def file_end(file, output) + end + end + end +end diff --git a/lib/hydra/listener/minimal_output.rb b/lib/hydra/listener/minimal_output.rb new file mode 100644 index 0000000..9e72dba --- /dev/null +++ b/lib/hydra/listener/minimal_output.rb @@ -0,0 +1,20 @@ +module Hydra #:nodoc: + module Listener #:nodoc: + # Minimal output listener. Outputs all the files at the start + # of testing and outputs a ./F/E per file. As well as + # full error output, if any. + class MinimalOutput < Hydra::Listener::Abstract + def testing_begin(files) + @output.write "Hydra Testing:\n#{files.inspect}\n" + end + + def testing_end + @output.write "\nHydra Completed\n" + end + + def file_end(file, output) + @output.write output + end + end + end +end diff --git a/lib/hydra/listener/report_generator.rb b/lib/hydra/listener/report_generator.rb new file mode 100644 index 0000000..53e5ae9 --- /dev/null +++ b/lib/hydra/listener/report_generator.rb @@ -0,0 +1,25 @@ +module Hydra #:nodoc: + module Listener #:nodoc: + # Output a textual report at the end of testing + class ReportGenerator < Hydra::Listener::Abstract + def testing_begin(files) + @report = { } + end + + def file_begin(file) + @report[file] ||= { } + @report[file]['start'] = Time.now.to_f + end + + def file_end(file, output) + @report[file]['end'] = Time.now.to_f + @report[file]['duration'] = @report[file]['end'] - @report[file]['start'] + end + + def testing_end + YAML.dump(@report, @output) + @output.close + end + end + end +end diff --git a/lib/hydra/master.rb b/lib/hydra/master.rb index c79b1c8..96da462 100644 --- a/lib/hydra/master.rb +++ b/lib/hydra/master.rb @@ -19,6 +19,13 @@ module Hydra #:nodoc: # * :workers # * An array of hashes. Each hash should be the configuration options # for a worker. + # * :listeners + # * An array of Hydra::Listener objects. See Hydra::Listener::MinimalOutput for an + # example listener + # * :verbose + # * Set to true to see lots of Hydra output (for debugging) + # * :autosort + # * Set to false to disable automatic sorting by historical run-time per file def initialize(opts = { }) opts.stringify_keys! config_file = opts.delete('config') { nil } @@ -30,13 +37,16 @@ module Hydra #:nodoc: @incomplete_files = @files.dup @workers = [] @listeners = [] + @event_listeners = Array(opts.fetch('listeners') { nil } ) @verbose = opts.fetch('verbose') { false } - @report = opts.fetch('report') { false } @autosort = opts.fetch('autosort') { true } - sort_files_from_report if @autosort - init_report_file @sync = opts.fetch('sync') { nil } + if @autosort + sort_files_from_report + @event_listeners << Hydra::Listener::ReportGenerator.new(File.new(heuristic_file, 'w')) + end + # default is one worker that is configured to use a pipe with one runner worker_cfg = opts.fetch('workers') { [ { 'type' => 'local', 'runners' => 1} ] } @@ -45,6 +55,8 @@ module Hydra #:nodoc: trace " Workers: (#{worker_cfg.inspect})" trace " Verbose: (#{@verbose.inspect})" + @event_listeners.each{|l| l.testing_begin(@files) } + boot_workers worker_cfg process_messages end @@ -56,7 +68,7 @@ module Hydra #:nodoc: f = @files.shift if f trace "Sending #{f.inspect}" - report_start_time(f) + @event_listeners.each{|l| l.file_begin(f) } worker[:io].write(RunFile.new(:file => f)) else trace "No more files to send" @@ -65,11 +77,9 @@ module Hydra #:nodoc: # Process the results coming back from the worker. def process_results(worker, message) - $stdout.write message.output - # only delete one @incomplete_files.delete_at(@incomplete_files.index(message.file)) trace "#{@incomplete_files.size} Files Remaining" - report_finish_time(message.file) + @event_listeners.each{|l| l.file_end(message.file, message.output) } if @incomplete_files.empty? shutdown_all_workers else @@ -185,60 +195,25 @@ module Hydra #:nodoc: end @listeners.each{|l| l.join} - - generate_report - end - - def init_report_file - FileUtils.rm_f(report_file) - FileUtils.rm_f(report_results_file) - end - - def report_start_time(file) - File.open(report_file, 'a'){|f| f.write "#{file}|start|#{Time.now.to_f}\n" } - end - - def report_finish_time(file) - File.open(report_file, 'a'){|f| f.write "#{file}|finish|#{Time.now.to_f}\n" } - end - - def generate_report - report = {} - lines = nil - File.open(report_file, 'r'){|f| lines = f.read.split("\n")} - lines.each{|l| l = l.split('|'); report[l[0]] ||= {}; report[l[0]][l[1]] = l[2]} - report.each{|file, times| report[file]['duration'] = times['finish'].to_f - times['start'].to_f} - report = report.sort{|a, b| b[1]['duration'] <=> a[1]['duration']} - output = [] - report.each{|file, times| output << "%.2f\t#{file}" % times['duration']} - @report_text = output.join("\n") - File.open(report_results_file, 'w'){|f| f.write @report_text} - return report_text - end - - def reported_files - return [] unless File.exists?(report_results_file) - rep = [] - File.open(report_results_file, 'r') do |f| - lines = f.read.split("\n") - lines.each{|l| rep << l.split(" ")[1] } - end - return rep + @event_listeners.each{|l| l.testing_end} end def sort_files_from_report - sorted_files = reported_files - reported_files.each do |f| - @files.push(@files.delete_at(@files.index(f))) if @files.index(f) + if File.exists? heuristic_file + report = YAML.load_file(heuristic_file) + return unless report + sorted_files = report.sort{ |a,b| + b[1]['duration'] <=> a[1]['duration'] + }.collect{|tuple| tuple[0]} + + sorted_files.each do |f| + @files.push(@files.delete_at(@files.index(f))) if @files.index(f) + end end end - def report_file - @report_file ||= File.join(Dir.tmpdir, 'hydra_report.txt') - end - - def report_results_file - @report_results_file ||= File.join(Dir.tmpdir, 'hydra_report_results.txt') + def heuristic_file + @heuristic_file ||= File.join(Dir.tmpdir, 'hydra_heuristics.yml') end end end diff --git a/lib/hydra/tasks.rb b/lib/hydra/tasks.rb index f2de202..5fe904c 100644 --- a/lib/hydra/tasks.rb +++ b/lib/hydra/tasks.rb @@ -56,7 +56,7 @@ module Hydra #:nodoc: # t.add_files 'test/functional/**/*_test.rb' # t.add_files 'test/integration/**/*_test.rb' # t.verbose = false # optionally set to true for lots of debug messages - # t.report = true # optionally set to true for a final report of test times + # t.autosort = false # disable automatic sorting based on runtime of tests # end class TestTask < Hydra::Task @@ -65,8 +65,8 @@ module Hydra #:nodoc: @name = name @files = [] @verbose = false - @report = false @autosort = true + @listeners = [Hydra::Listener::MinimalOutput.new] yield self if block_given? @@ -74,9 +74,9 @@ module Hydra #:nodoc: @opts = { :verbose => @verbose, - :report => @report, :autosort => @autosort, - :files => @files + :files => @files, + :listeners => @listeners } if @config @opts.merge!(:config => @config) @@ -92,10 +92,7 @@ module Hydra #:nodoc: def define desc "Hydra Tests" + (@name == :hydra ? "" : " for #{@name}") task @name do - $stdout.write "Hydra Testing #{files.inspect}\n" - h = Hydra::Master.new(@opts) - $stdout.write "\n"+h.report_text if @report - $stdout.write "\nHydra Completed\n" + Hydra::Master.new(@opts) exit(0) #bypass test on_exit output end end diff --git a/test/master_test.rb b/test/master_test.rb index 90d5696..8f7acf7 100644 --- a/test/master_test.rb +++ b/test/master_test.rb @@ -21,15 +21,13 @@ class MasterTest < Test::Unit::TestCase end should "generate a report" do - Hydra::Master.new( - :files => [test_file], - :report => true - ) + Hydra::Master.new(:files => [test_file]) assert File.exists?(target_file) assert_equal "HYDRA", File.read(target_file) - report_file = File.join(Dir.tmpdir, 'hydra_report.txt') + report_file = File.join(Dir.tmpdir, 'hydra_heuristics.yml') assert File.exists?(report_file) - assert_equal 2, File.read(report_file).split("\n").size + assert report = YAML.load_file(report_file) + assert_not_nil report[test_file] end should "run a test 6 times on 1 worker with 2 runners" do @@ -119,19 +117,13 @@ class MasterTest < Test::Unit::TestCase # ensure b is on remote assert File.exists?(File.join(remote, 'test_b.rb')), "B should be on remote" - # fake as if the test got run, so only the sync code is really being tested - fake_result = Hydra::Messages::Worker::Results.new( - :file => 'test_a.rb', :output => '.' - ).serialize.inspect - Hydra::Master.new( :files => ['test_a.rb'], :workers => [{ :type => :ssh, :connect => 'localhost', :directory => remote, - :runners => 1, - :command => "ruby -e 'puts #{fake_result}' && exit" + :runners => 1 }], :sync => { :directory => local,