added even listeners, and replaced output and reporting system with listeners

This commit is contained in:
Nick Gauthier 2010-03-25 12:15:01 -04:00
parent fd5299f8a9
commit 2f2919c156
8 changed files with 123 additions and 77 deletions

View File

@ -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",

View File

@ -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'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,