added even listeners, and replaced output and reporting system with listeners
This commit is contained in:
parent
fd5299f8a9
commit
2f2919c156
|
@ -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.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
||||||
s.authors = ["Nick Gauthier"]
|
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.description = %q{Spread your tests over multiple machines to test your code faster.}
|
||||||
s.email = %q{nick@smartlogicsolutions.com}
|
s.email = %q{nick@smartlogicsolutions.com}
|
||||||
s.extra_rdoc_files = [
|
s.extra_rdoc_files = [
|
||||||
|
@ -30,6 +30,9 @@ Gem::Specification.new do |s|
|
||||||
"hydra_gray.png",
|
"hydra_gray.png",
|
||||||
"lib/hydra.rb",
|
"lib/hydra.rb",
|
||||||
"lib/hydra/hash.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/master.rb",
|
||||||
"lib/hydra/message.rb",
|
"lib/hydra/message.rb",
|
||||||
"lib/hydra/message/master_messages.rb",
|
"lib/hydra/message/master_messages.rb",
|
||||||
|
|
|
@ -7,4 +7,8 @@ require 'hydra/safe_fork'
|
||||||
require 'hydra/runner'
|
require 'hydra/runner'
|
||||||
require 'hydra/worker'
|
require 'hydra/worker'
|
||||||
require 'hydra/master'
|
require 'hydra/master'
|
||||||
|
require 'hydra/listener/abstract'
|
||||||
|
require 'hydra/listener/minimal_output'
|
||||||
|
require 'hydra/listener/report_generator'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -19,6 +19,13 @@ module Hydra #:nodoc:
|
||||||
# * :workers
|
# * :workers
|
||||||
# * An array of hashes. Each hash should be the configuration options
|
# * An array of hashes. Each hash should be the configuration options
|
||||||
# for a worker.
|
# 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 = { })
|
def initialize(opts = { })
|
||||||
opts.stringify_keys!
|
opts.stringify_keys!
|
||||||
config_file = opts.delete('config') { nil }
|
config_file = opts.delete('config') { nil }
|
||||||
|
@ -30,13 +37,16 @@ module Hydra #:nodoc:
|
||||||
@incomplete_files = @files.dup
|
@incomplete_files = @files.dup
|
||||||
@workers = []
|
@workers = []
|
||||||
@listeners = []
|
@listeners = []
|
||||||
|
@event_listeners = Array(opts.fetch('listeners') { nil } )
|
||||||
@verbose = opts.fetch('verbose') { false }
|
@verbose = opts.fetch('verbose') { false }
|
||||||
@report = opts.fetch('report') { false }
|
|
||||||
@autosort = opts.fetch('autosort') { true }
|
@autosort = opts.fetch('autosort') { true }
|
||||||
sort_files_from_report if @autosort
|
|
||||||
init_report_file
|
|
||||||
@sync = opts.fetch('sync') { nil }
|
@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
|
# default is one worker that is configured to use a pipe with one runner
|
||||||
worker_cfg = opts.fetch('workers') { [ { 'type' => 'local', 'runners' => 1} ] }
|
worker_cfg = opts.fetch('workers') { [ { 'type' => 'local', 'runners' => 1} ] }
|
||||||
|
|
||||||
|
@ -45,6 +55,8 @@ module Hydra #:nodoc:
|
||||||
trace " Workers: (#{worker_cfg.inspect})"
|
trace " Workers: (#{worker_cfg.inspect})"
|
||||||
trace " Verbose: (#{@verbose.inspect})"
|
trace " Verbose: (#{@verbose.inspect})"
|
||||||
|
|
||||||
|
@event_listeners.each{|l| l.testing_begin(@files) }
|
||||||
|
|
||||||
boot_workers worker_cfg
|
boot_workers worker_cfg
|
||||||
process_messages
|
process_messages
|
||||||
end
|
end
|
||||||
|
@ -56,7 +68,7 @@ module Hydra #:nodoc:
|
||||||
f = @files.shift
|
f = @files.shift
|
||||||
if f
|
if f
|
||||||
trace "Sending #{f.inspect}"
|
trace "Sending #{f.inspect}"
|
||||||
report_start_time(f)
|
@event_listeners.each{|l| l.file_begin(f) }
|
||||||
worker[:io].write(RunFile.new(:file => f))
|
worker[:io].write(RunFile.new(:file => f))
|
||||||
else
|
else
|
||||||
trace "No more files to send"
|
trace "No more files to send"
|
||||||
|
@ -65,11 +77,9 @@ module Hydra #:nodoc:
|
||||||
|
|
||||||
# Process the results coming back from the worker.
|
# Process the results coming back from the worker.
|
||||||
def process_results(worker, message)
|
def process_results(worker, message)
|
||||||
$stdout.write message.output
|
|
||||||
# only delete one
|
|
||||||
@incomplete_files.delete_at(@incomplete_files.index(message.file))
|
@incomplete_files.delete_at(@incomplete_files.index(message.file))
|
||||||
trace "#{@incomplete_files.size} Files Remaining"
|
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?
|
if @incomplete_files.empty?
|
||||||
shutdown_all_workers
|
shutdown_all_workers
|
||||||
else
|
else
|
||||||
|
@ -185,60 +195,25 @@ module Hydra #:nodoc:
|
||||||
end
|
end
|
||||||
|
|
||||||
@listeners.each{|l| l.join}
|
@listeners.each{|l| l.join}
|
||||||
|
@event_listeners.each{|l| l.testing_end}
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def sort_files_from_report
|
def sort_files_from_report
|
||||||
sorted_files = reported_files
|
if File.exists? heuristic_file
|
||||||
reported_files.each do |f|
|
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)
|
@files.push(@files.delete_at(@files.index(f))) if @files.index(f)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def report_file
|
|
||||||
@report_file ||= File.join(Dir.tmpdir, 'hydra_report.txt')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def report_results_file
|
def heuristic_file
|
||||||
@report_results_file ||= File.join(Dir.tmpdir, 'hydra_report_results.txt')
|
@heuristic_file ||= File.join(Dir.tmpdir, 'hydra_heuristics.yml')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -56,7 +56,7 @@ module Hydra #:nodoc:
|
||||||
# t.add_files 'test/functional/**/*_test.rb'
|
# t.add_files 'test/functional/**/*_test.rb'
|
||||||
# t.add_files 'test/integration/**/*_test.rb'
|
# t.add_files 'test/integration/**/*_test.rb'
|
||||||
# t.verbose = false # optionally set to true for lots of debug messages
|
# 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
|
# end
|
||||||
class TestTask < Hydra::Task
|
class TestTask < Hydra::Task
|
||||||
|
|
||||||
|
@ -65,8 +65,8 @@ module Hydra #:nodoc:
|
||||||
@name = name
|
@name = name
|
||||||
@files = []
|
@files = []
|
||||||
@verbose = false
|
@verbose = false
|
||||||
@report = false
|
|
||||||
@autosort = true
|
@autosort = true
|
||||||
|
@listeners = [Hydra::Listener::MinimalOutput.new]
|
||||||
|
|
||||||
yield self if block_given?
|
yield self if block_given?
|
||||||
|
|
||||||
|
@ -74,9 +74,9 @@ module Hydra #:nodoc:
|
||||||
|
|
||||||
@opts = {
|
@opts = {
|
||||||
:verbose => @verbose,
|
:verbose => @verbose,
|
||||||
:report => @report,
|
|
||||||
:autosort => @autosort,
|
:autosort => @autosort,
|
||||||
:files => @files
|
:files => @files,
|
||||||
|
:listeners => @listeners
|
||||||
}
|
}
|
||||||
if @config
|
if @config
|
||||||
@opts.merge!(:config => @config)
|
@opts.merge!(:config => @config)
|
||||||
|
@ -92,10 +92,7 @@ module Hydra #:nodoc:
|
||||||
def define
|
def define
|
||||||
desc "Hydra Tests" + (@name == :hydra ? "" : " for #{@name}")
|
desc "Hydra Tests" + (@name == :hydra ? "" : " for #{@name}")
|
||||||
task @name do
|
task @name do
|
||||||
$stdout.write "Hydra Testing #{files.inspect}\n"
|
Hydra::Master.new(@opts)
|
||||||
h = Hydra::Master.new(@opts)
|
|
||||||
$stdout.write "\n"+h.report_text if @report
|
|
||||||
$stdout.write "\nHydra Completed\n"
|
|
||||||
exit(0) #bypass test on_exit output
|
exit(0) #bypass test on_exit output
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,15 +21,13 @@ class MasterTest < Test::Unit::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
should "generate a report" do
|
should "generate a report" do
|
||||||
Hydra::Master.new(
|
Hydra::Master.new(:files => [test_file])
|
||||||
:files => [test_file],
|
|
||||||
:report => true
|
|
||||||
)
|
|
||||||
assert File.exists?(target_file)
|
assert File.exists?(target_file)
|
||||||
assert_equal "HYDRA", File.read(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 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
|
end
|
||||||
|
|
||||||
should "run a test 6 times on 1 worker with 2 runners" do
|
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
|
# ensure b is on remote
|
||||||
assert File.exists?(File.join(remote, 'test_b.rb')), "B should be 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(
|
Hydra::Master.new(
|
||||||
:files => ['test_a.rb'],
|
:files => ['test_a.rb'],
|
||||||
:workers => [{
|
:workers => [{
|
||||||
:type => :ssh,
|
:type => :ssh,
|
||||||
:connect => 'localhost',
|
:connect => 'localhost',
|
||||||
:directory => remote,
|
:directory => remote,
|
||||||
:runners => 1,
|
:runners => 1
|
||||||
:command => "ruby -e 'puts #{fake_result}' && exit"
|
|
||||||
}],
|
}],
|
||||||
:sync => {
|
:sync => {
|
||||||
:directory => local,
|
:directory => local,
|
||||||
|
|
Loading…
Reference in New Issue