hydra 0.12.0 → 0.13.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/TODO +13 -3
- data/VERSION +1 -1
- data/hydra.gemspec +5 -2
- data/lib/hydra.rb +4 -0
- data/lib/hydra/listener/abstract.rb +30 -0
- data/lib/hydra/listener/minimal_output.rb +24 -0
- data/lib/hydra/listener/report_generator.rb +30 -0
- data/lib/hydra/master.rb +30 -55
- data/lib/hydra/tasks.rb +5 -8
- data/test/master_test.rb +5 -13
- metadata +5 -2
data/TODO
CHANGED
@@ -1,8 +1,18 @@
|
|
1
1
|
= Hydra TODO
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
* hydra:sync task that runs the SSH syncing for remote workers
|
4
|
+
* ensure same version is running remotely (gem directive)
|
5
|
+
* on a crash, bubble up error messages
|
6
|
+
* send workers a "boot" message with all the files that will be tested so that it
|
7
|
+
can boot the environment before forking runners
|
8
|
+
* named configurations (i.e. 'local', 'remote', 'myconfig') so users can swap configs with an
|
9
|
+
environment variable or with a hydra testtask option
|
5
10
|
|
6
|
-
|
11
|
+
== Reporting
|
7
12
|
|
13
|
+
Refactor reporting into an event listening system. Add in a default listener that messages:
|
14
|
+
|
15
|
+
* Files at start
|
16
|
+
* Progress status "50% (10/20 files)"
|
17
|
+
* Time report at the end
|
8
18
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.13.0
|
data/hydra.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{hydra}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.13.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Nick Gauthier"]
|
12
|
-
s.date = %q{2010-
|
12
|
+
s.date = %q{2010-03-25}
|
13
13
|
s.description = %q{Spread your tests over multiple machines to test your code faster.}
|
14
14
|
s.email = %q{nick@smartlogicsolutions.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -30,6 +30,9 @@ Gem::Specification.new do |s|
|
|
30
30
|
"hydra_gray.png",
|
31
31
|
"lib/hydra.rb",
|
32
32
|
"lib/hydra/hash.rb",
|
33
|
+
"lib/hydra/listener/abstract.rb",
|
34
|
+
"lib/hydra/listener/minimal_output.rb",
|
35
|
+
"lib/hydra/listener/report_generator.rb",
|
33
36
|
"lib/hydra/master.rb",
|
34
37
|
"lib/hydra/message.rb",
|
35
38
|
"lib/hydra/message/master_messages.rb",
|
data/lib/hydra.rb
CHANGED
@@ -0,0 +1,30 @@
|
|
1
|
+
module Hydra #:nodoc:
|
2
|
+
module Listener #:nodoc:
|
3
|
+
# Abstract listener that implements all the events
|
4
|
+
# but does nothing.
|
5
|
+
class Abstract
|
6
|
+
# Create a new listener.
|
7
|
+
#
|
8
|
+
# Output: The IO object for outputting any information.
|
9
|
+
# Defaults to STDOUT, but you could pass a file in, or STDERR
|
10
|
+
def initialize(output = $stdout)
|
11
|
+
@output = output
|
12
|
+
end
|
13
|
+
# Fired when testing has started
|
14
|
+
def testing_begin(files)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Fired when testing finishes
|
18
|
+
def testing_end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Fired when a file is started
|
22
|
+
def file_begin(file)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Fired when a file is finished
|
26
|
+
def file_end(file, output)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Hydra #:nodoc:
|
2
|
+
module Listener #:nodoc:
|
3
|
+
# Minimal output listener. Outputs all the files at the start
|
4
|
+
# of testing and outputs a ./F/E per file. As well as
|
5
|
+
# full error output, if any.
|
6
|
+
class MinimalOutput < Hydra::Listener::Abstract
|
7
|
+
# output a starting message
|
8
|
+
def testing_begin(files)
|
9
|
+
@output.write "Hydra Testing:\n#{files.inspect}\n"
|
10
|
+
end
|
11
|
+
|
12
|
+
# output a finished message
|
13
|
+
def testing_end
|
14
|
+
@output.write "\nHydra Completed\n"
|
15
|
+
end
|
16
|
+
|
17
|
+
# For each file, just output a . for a successful file, or the
|
18
|
+
# Failure/Error output from the tests
|
19
|
+
def file_end(file, output)
|
20
|
+
@output.write output
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Hydra #:nodoc:
|
2
|
+
module Listener #:nodoc:
|
3
|
+
# Output a textual report at the end of testing
|
4
|
+
class ReportGenerator < Hydra::Listener::Abstract
|
5
|
+
# Initialize a new report
|
6
|
+
def testing_begin(files)
|
7
|
+
@report = { }
|
8
|
+
end
|
9
|
+
|
10
|
+
# Log the start time of a file
|
11
|
+
def file_begin(file)
|
12
|
+
@report[file] ||= { }
|
13
|
+
@report[file]['start'] = Time.now.to_f
|
14
|
+
end
|
15
|
+
|
16
|
+
# Log the end time of a file and compute the file's testing
|
17
|
+
# duration
|
18
|
+
def file_end(file, output)
|
19
|
+
@report[file]['end'] = Time.now.to_f
|
20
|
+
@report[file]['duration'] = @report[file]['end'] - @report[file]['start']
|
21
|
+
end
|
22
|
+
|
23
|
+
# output the report
|
24
|
+
def testing_end
|
25
|
+
YAML.dump(@report, @output)
|
26
|
+
@output.close
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/hydra/master.rb
CHANGED
@@ -19,6 +19,13 @@ module Hydra #:nodoc:
|
|
19
19
|
# * :workers
|
20
20
|
# * An array of hashes. Each hash should be the configuration options
|
21
21
|
# for a worker.
|
22
|
+
# * :listeners
|
23
|
+
# * An array of Hydra::Listener objects. See Hydra::Listener::MinimalOutput for an
|
24
|
+
# example listener
|
25
|
+
# * :verbose
|
26
|
+
# * Set to true to see lots of Hydra output (for debugging)
|
27
|
+
# * :autosort
|
28
|
+
# * Set to false to disable automatic sorting by historical run-time per file
|
22
29
|
def initialize(opts = { })
|
23
30
|
opts.stringify_keys!
|
24
31
|
config_file = opts.delete('config') { nil }
|
@@ -30,13 +37,16 @@ module Hydra #:nodoc:
|
|
30
37
|
@incomplete_files = @files.dup
|
31
38
|
@workers = []
|
32
39
|
@listeners = []
|
40
|
+
@event_listeners = Array(opts.fetch('listeners') { nil } )
|
33
41
|
@verbose = opts.fetch('verbose') { false }
|
34
|
-
@report = opts.fetch('report') { false }
|
35
42
|
@autosort = opts.fetch('autosort') { true }
|
36
|
-
sort_files_from_report if @autosort
|
37
|
-
init_report_file
|
38
43
|
@sync = opts.fetch('sync') { nil }
|
39
44
|
|
45
|
+
if @autosort
|
46
|
+
sort_files_from_report
|
47
|
+
@event_listeners << Hydra::Listener::ReportGenerator.new(File.new(heuristic_file, 'w'))
|
48
|
+
end
|
49
|
+
|
40
50
|
# default is one worker that is configured to use a pipe with one runner
|
41
51
|
worker_cfg = opts.fetch('workers') { [ { 'type' => 'local', 'runners' => 1} ] }
|
42
52
|
|
@@ -45,6 +55,8 @@ module Hydra #:nodoc:
|
|
45
55
|
trace " Workers: (#{worker_cfg.inspect})"
|
46
56
|
trace " Verbose: (#{@verbose.inspect})"
|
47
57
|
|
58
|
+
@event_listeners.each{|l| l.testing_begin(@files) }
|
59
|
+
|
48
60
|
boot_workers worker_cfg
|
49
61
|
process_messages
|
50
62
|
end
|
@@ -56,7 +68,7 @@ module Hydra #:nodoc:
|
|
56
68
|
f = @files.shift
|
57
69
|
if f
|
58
70
|
trace "Sending #{f.inspect}"
|
59
|
-
|
71
|
+
@event_listeners.each{|l| l.file_begin(f) }
|
60
72
|
worker[:io].write(RunFile.new(:file => f))
|
61
73
|
else
|
62
74
|
trace "No more files to send"
|
@@ -65,11 +77,9 @@ module Hydra #:nodoc:
|
|
65
77
|
|
66
78
|
# Process the results coming back from the worker.
|
67
79
|
def process_results(worker, message)
|
68
|
-
$stdout.write message.output
|
69
|
-
# only delete one
|
70
80
|
@incomplete_files.delete_at(@incomplete_files.index(message.file))
|
71
81
|
trace "#{@incomplete_files.size} Files Remaining"
|
72
|
-
|
82
|
+
@event_listeners.each{|l| l.file_end(message.file, message.output) }
|
73
83
|
if @incomplete_files.empty?
|
74
84
|
shutdown_all_workers
|
75
85
|
else
|
@@ -185,60 +195,25 @@ module Hydra #:nodoc:
|
|
185
195
|
end
|
186
196
|
|
187
197
|
@listeners.each{|l| l.join}
|
188
|
-
|
189
|
-
generate_report
|
190
|
-
end
|
191
|
-
|
192
|
-
def init_report_file
|
193
|
-
FileUtils.rm_f(report_file)
|
194
|
-
FileUtils.rm_f(report_results_file)
|
195
|
-
end
|
196
|
-
|
197
|
-
def report_start_time(file)
|
198
|
-
File.open(report_file, 'a'){|f| f.write "#{file}|start|#{Time.now.to_f}\n" }
|
199
|
-
end
|
200
|
-
|
201
|
-
def report_finish_time(file)
|
202
|
-
File.open(report_file, 'a'){|f| f.write "#{file}|finish|#{Time.now.to_f}\n" }
|
203
|
-
end
|
204
|
-
|
205
|
-
def generate_report
|
206
|
-
report = {}
|
207
|
-
lines = nil
|
208
|
-
File.open(report_file, 'r'){|f| lines = f.read.split("\n")}
|
209
|
-
lines.each{|l| l = l.split('|'); report[l[0]] ||= {}; report[l[0]][l[1]] = l[2]}
|
210
|
-
report.each{|file, times| report[file]['duration'] = times['finish'].to_f - times['start'].to_f}
|
211
|
-
report = report.sort{|a, b| b[1]['duration'] <=> a[1]['duration']}
|
212
|
-
output = []
|
213
|
-
report.each{|file, times| output << "%.2f\t#{file}" % times['duration']}
|
214
|
-
@report_text = output.join("\n")
|
215
|
-
File.open(report_results_file, 'w'){|f| f.write @report_text}
|
216
|
-
return report_text
|
217
|
-
end
|
218
|
-
|
219
|
-
def reported_files
|
220
|
-
return [] unless File.exists?(report_results_file)
|
221
|
-
rep = []
|
222
|
-
File.open(report_results_file, 'r') do |f|
|
223
|
-
lines = f.read.split("\n")
|
224
|
-
lines.each{|l| rep << l.split(" ")[1] }
|
225
|
-
end
|
226
|
-
return rep
|
198
|
+
@event_listeners.each{|l| l.testing_end}
|
227
199
|
end
|
228
200
|
|
229
201
|
def sort_files_from_report
|
230
|
-
|
231
|
-
|
232
|
-
|
202
|
+
if File.exists? heuristic_file
|
203
|
+
report = YAML.load_file(heuristic_file)
|
204
|
+
return unless report
|
205
|
+
sorted_files = report.sort{ |a,b|
|
206
|
+
b[1]['duration'] <=> a[1]['duration']
|
207
|
+
}.collect{|tuple| tuple[0]}
|
208
|
+
|
209
|
+
sorted_files.each do |f|
|
210
|
+
@files.push(@files.delete_at(@files.index(f))) if @files.index(f)
|
211
|
+
end
|
233
212
|
end
|
234
213
|
end
|
235
214
|
|
236
|
-
def
|
237
|
-
@
|
238
|
-
end
|
239
|
-
|
240
|
-
def report_results_file
|
241
|
-
@report_results_file ||= File.join(Dir.tmpdir, 'hydra_report_results.txt')
|
215
|
+
def heuristic_file
|
216
|
+
@heuristic_file ||= File.join(Dir.tmpdir, 'hydra_heuristics.yml')
|
242
217
|
end
|
243
218
|
end
|
244
219
|
end
|
data/lib/hydra/tasks.rb
CHANGED
@@ -56,7 +56,7 @@ module Hydra #:nodoc:
|
|
56
56
|
# t.add_files 'test/functional/**/*_test.rb'
|
57
57
|
# t.add_files 'test/integration/**/*_test.rb'
|
58
58
|
# t.verbose = false # optionally set to true for lots of debug messages
|
59
|
-
# t.
|
59
|
+
# t.autosort = false # disable automatic sorting based on runtime of tests
|
60
60
|
# end
|
61
61
|
class TestTask < Hydra::Task
|
62
62
|
|
@@ -65,8 +65,8 @@ module Hydra #:nodoc:
|
|
65
65
|
@name = name
|
66
66
|
@files = []
|
67
67
|
@verbose = false
|
68
|
-
@report = false
|
69
68
|
@autosort = true
|
69
|
+
@listeners = [Hydra::Listener::MinimalOutput.new]
|
70
70
|
|
71
71
|
yield self if block_given?
|
72
72
|
|
@@ -74,9 +74,9 @@ module Hydra #:nodoc:
|
|
74
74
|
|
75
75
|
@opts = {
|
76
76
|
:verbose => @verbose,
|
77
|
-
:report => @report,
|
78
77
|
:autosort => @autosort,
|
79
|
-
:files => @files
|
78
|
+
:files => @files,
|
79
|
+
:listeners => @listeners
|
80
80
|
}
|
81
81
|
if @config
|
82
82
|
@opts.merge!(:config => @config)
|
@@ -92,10 +92,7 @@ module Hydra #:nodoc:
|
|
92
92
|
def define
|
93
93
|
desc "Hydra Tests" + (@name == :hydra ? "" : " for #{@name}")
|
94
94
|
task @name do
|
95
|
-
|
96
|
-
h = Hydra::Master.new(@opts)
|
97
|
-
$stdout.write "\n"+h.report_text if @report
|
98
|
-
$stdout.write "\nHydra Completed\n"
|
95
|
+
Hydra::Master.new(@opts)
|
99
96
|
exit(0) #bypass test on_exit output
|
100
97
|
end
|
101
98
|
end
|
data/test/master_test.rb
CHANGED
@@ -21,15 +21,13 @@ class MasterTest < Test::Unit::TestCase
|
|
21
21
|
end
|
22
22
|
|
23
23
|
should "generate a report" do
|
24
|
-
Hydra::Master.new(
|
25
|
-
:files => [test_file],
|
26
|
-
:report => true
|
27
|
-
)
|
24
|
+
Hydra::Master.new(:files => [test_file])
|
28
25
|
assert File.exists?(target_file)
|
29
26
|
assert_equal "HYDRA", File.read(target_file)
|
30
|
-
report_file = File.join(Dir.tmpdir, '
|
27
|
+
report_file = File.join(Dir.tmpdir, 'hydra_heuristics.yml')
|
31
28
|
assert File.exists?(report_file)
|
32
|
-
|
29
|
+
assert report = YAML.load_file(report_file)
|
30
|
+
assert_not_nil report[test_file]
|
33
31
|
end
|
34
32
|
|
35
33
|
should "run a test 6 times on 1 worker with 2 runners" do
|
@@ -119,19 +117,13 @@ class MasterTest < Test::Unit::TestCase
|
|
119
117
|
# ensure b is on remote
|
120
118
|
assert File.exists?(File.join(remote, 'test_b.rb')), "B should be on remote"
|
121
119
|
|
122
|
-
# fake as if the test got run, so only the sync code is really being tested
|
123
|
-
fake_result = Hydra::Messages::Worker::Results.new(
|
124
|
-
:file => 'test_a.rb', :output => '.'
|
125
|
-
).serialize.inspect
|
126
|
-
|
127
120
|
Hydra::Master.new(
|
128
121
|
:files => ['test_a.rb'],
|
129
122
|
:workers => [{
|
130
123
|
:type => :ssh,
|
131
124
|
:connect => 'localhost',
|
132
125
|
:directory => remote,
|
133
|
-
:runners => 1
|
134
|
-
:command => "ruby -e 'puts #{fake_result}' && exit"
|
126
|
+
:runners => 1
|
135
127
|
}],
|
136
128
|
:sync => {
|
137
129
|
:directory => local,
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hydra
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.13.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Gauthier
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-
|
12
|
+
date: 2010-03-25 00:00:00 -04:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -45,6 +45,9 @@ files:
|
|
45
45
|
- hydra_gray.png
|
46
46
|
- lib/hydra.rb
|
47
47
|
- lib/hydra/hash.rb
|
48
|
+
- lib/hydra/listener/abstract.rb
|
49
|
+
- lib/hydra/listener/minimal_output.rb
|
50
|
+
- lib/hydra/listener/report_generator.rb
|
48
51
|
- lib/hydra/master.rb
|
49
52
|
- lib/hydra/message.rb
|
50
53
|
- lib/hydra/message/master_messages.rb
|