causes-hydra 0.21.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/.document +5 -0
- data/.gitignore +22 -0
- data/LICENSE +20 -0
- data/README.rdoc +39 -0
- data/Rakefile +56 -0
- data/TODO +18 -0
- data/VERSION +1 -0
- data/bin/warmsnake.rb +76 -0
- data/caliper.yml +6 -0
- data/hydra-icon-64x64.png +0 -0
- data/hydra.gemspec +130 -0
- data/hydra_gray.png +0 -0
- data/lib/hydra.rb +16 -0
- data/lib/hydra/cucumber/formatter.rb +29 -0
- data/lib/hydra/hash.rb +16 -0
- data/lib/hydra/js/lint.js +5150 -0
- data/lib/hydra/listener/abstract.rb +39 -0
- data/lib/hydra/listener/minimal_output.rb +24 -0
- data/lib/hydra/listener/notifier.rb +17 -0
- data/lib/hydra/listener/progress_bar.rb +48 -0
- data/lib/hydra/listener/report_generator.rb +30 -0
- data/lib/hydra/master.rb +249 -0
- data/lib/hydra/message.rb +47 -0
- data/lib/hydra/message/master_messages.rb +19 -0
- data/lib/hydra/message/runner_messages.rb +52 -0
- data/lib/hydra/message/worker_messages.rb +52 -0
- data/lib/hydra/messaging_io.rb +46 -0
- data/lib/hydra/pipe.rb +61 -0
- data/lib/hydra/runner.rb +305 -0
- data/lib/hydra/safe_fork.rb +31 -0
- data/lib/hydra/spec/autorun_override.rb +3 -0
- data/lib/hydra/spec/hydra_formatter.rb +26 -0
- data/lib/hydra/ssh.rb +41 -0
- data/lib/hydra/stdio.rb +16 -0
- data/lib/hydra/sync.rb +99 -0
- data/lib/hydra/tasks.rb +342 -0
- data/lib/hydra/trace.rb +24 -0
- data/lib/hydra/worker.rb +150 -0
- data/test/fixtures/assert_true.rb +7 -0
- data/test/fixtures/config.yml +4 -0
- data/test/fixtures/features/step_definitions.rb +21 -0
- data/test/fixtures/features/write_alternate_file.feature +7 -0
- data/test/fixtures/features/write_file.feature +7 -0
- data/test/fixtures/hello_world.rb +3 -0
- data/test/fixtures/js_file.js +4 -0
- data/test/fixtures/json_data.json +4 -0
- data/test/fixtures/slow.rb +9 -0
- data/test/fixtures/sync_test.rb +8 -0
- data/test/fixtures/write_file.rb +10 -0
- data/test/fixtures/write_file_alternate_spec.rb +10 -0
- data/test/fixtures/write_file_spec.rb +9 -0
- data/test/fixtures/write_file_with_pending_spec.rb +11 -0
- data/test/master_test.rb +152 -0
- data/test/message_test.rb +31 -0
- data/test/pipe_test.rb +38 -0
- data/test/runner_test.rb +153 -0
- data/test/ssh_test.rb +14 -0
- data/test/sync_test.rb +113 -0
- data/test/test_helper.rb +68 -0
- data/test/worker_test.rb +60 -0
- metadata +209 -0
data/lib/hydra/trace.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module Hydra #:nodoc:
|
2
|
+
# Trace output when in verbose mode.
|
3
|
+
module Trace
|
4
|
+
module ClassMethods
|
5
|
+
# Make a class traceable. Takes one parameter,
|
6
|
+
# which is the prefix for the trace to identify this class
|
7
|
+
def traceable(prefix = self.class.to_s)
|
8
|
+
include Hydra::Trace::InstanceMethods
|
9
|
+
class << self; attr_accessor :_traceable_prefix; end
|
10
|
+
self._traceable_prefix = prefix
|
11
|
+
$stdout.sync = true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module InstanceMethods
|
16
|
+
# Trace some output with the class's prefix and a newline.
|
17
|
+
# Checks to ensure we're running verbosely.
|
18
|
+
def trace(str)
|
19
|
+
$stdout.write "#{Time.now.to_f} #{self.class._traceable_prefix}| #{str}\n" if @verbose
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
Object.extend(Hydra::Trace::ClassMethods)
|
data/lib/hydra/worker.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
module Hydra #:nodoc:
|
2
|
+
# Hydra class responsible to dispatching runners and communicating with the master.
|
3
|
+
#
|
4
|
+
# The Worker is never run directly by a user. Workers are created by a
|
5
|
+
# Master to delegate to Runners.
|
6
|
+
#
|
7
|
+
# The general convention is to have one Worker per machine on a distributed
|
8
|
+
# network.
|
9
|
+
class Worker
|
10
|
+
include Hydra::Messages::Worker
|
11
|
+
traceable('WORKER')
|
12
|
+
|
13
|
+
attr_reader :runners
|
14
|
+
# Create a new worker.
|
15
|
+
# * io: The IO object to use to communicate with the master
|
16
|
+
# * num_runners: The number of runners to launch
|
17
|
+
def initialize(opts = {})
|
18
|
+
@verbose = opts.fetch(:verbose) { false }
|
19
|
+
@io = opts.fetch(:io) { raise "No IO Object" }
|
20
|
+
@runners = []
|
21
|
+
@listeners = []
|
22
|
+
|
23
|
+
boot_runners(opts.fetch(:runners) { 1 })
|
24
|
+
@io.write(Hydra::Messages::Worker::WorkerBegin.new)
|
25
|
+
|
26
|
+
process_messages
|
27
|
+
|
28
|
+
@runners.each{|r| Process.wait r[:pid] }
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
# message handling methods
|
33
|
+
|
34
|
+
# When a runner wants a file, it hits this method with a message.
|
35
|
+
# Then the worker bubbles the file request up to the master.
|
36
|
+
def request_file(message, runner)
|
37
|
+
@io.write(RequestFile.new)
|
38
|
+
runner[:idle] = true
|
39
|
+
end
|
40
|
+
|
41
|
+
# When the master sends a file down to the worker, it hits this
|
42
|
+
# method. Then the worker delegates the file down to a runner.
|
43
|
+
def delegate_file(message)
|
44
|
+
runner = idle_runner
|
45
|
+
runner[:idle] = false
|
46
|
+
runner[:io].write(RunFile.new(eval(message.serialize)))
|
47
|
+
end
|
48
|
+
|
49
|
+
# When a runner finishes, it sends the results up to the worker. Then the
|
50
|
+
# worker sends the results up to the master.
|
51
|
+
def relay_results(message, runner)
|
52
|
+
runner[:idle] = true
|
53
|
+
@io.write(Results.new(eval(message.serialize)))
|
54
|
+
end
|
55
|
+
|
56
|
+
# When a master issues a shutdown order, it hits this method, which causes
|
57
|
+
# the worker to send shutdown messages to its runners.
|
58
|
+
def shutdown
|
59
|
+
@running = false
|
60
|
+
trace "Notifying #{@runners.size} Runners of Shutdown"
|
61
|
+
@runners.each do |r|
|
62
|
+
trace "Sending Shutdown to Runner"
|
63
|
+
trace "\t#{r.inspect}"
|
64
|
+
r[:io].write(Shutdown.new)
|
65
|
+
end
|
66
|
+
Thread.exit
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def boot_runners(num_runners) #:nodoc:
|
72
|
+
trace "Booting #{num_runners} Runners"
|
73
|
+
num_runners.times do
|
74
|
+
pipe = Hydra::Pipe.new
|
75
|
+
child = SafeFork.fork do
|
76
|
+
pipe.identify_as_child
|
77
|
+
Hydra::Runner.new(:io => pipe, :verbose => @verbose)
|
78
|
+
end
|
79
|
+
pipe.identify_as_parent
|
80
|
+
@runners << { :pid => child, :io => pipe, :idle => false }
|
81
|
+
end
|
82
|
+
trace "#{@runners.size} Runners booted"
|
83
|
+
end
|
84
|
+
|
85
|
+
# Continuously process messages
|
86
|
+
def process_messages #:nodoc:
|
87
|
+
trace "Processing Messages"
|
88
|
+
@running = true
|
89
|
+
|
90
|
+
Thread.abort_on_exception = true
|
91
|
+
|
92
|
+
process_messages_from_master
|
93
|
+
process_messages_from_runners
|
94
|
+
|
95
|
+
@listeners.each{|l| l.join }
|
96
|
+
@io.close
|
97
|
+
trace "Done processing messages"
|
98
|
+
end
|
99
|
+
|
100
|
+
def process_messages_from_master
|
101
|
+
@listeners << Thread.new do
|
102
|
+
while @running
|
103
|
+
begin
|
104
|
+
message = @io.gets
|
105
|
+
if message and !message.class.to_s.index("Master").nil?
|
106
|
+
trace "Received Message from Master"
|
107
|
+
trace "\t#{message.inspect}"
|
108
|
+
message.handle(self)
|
109
|
+
else
|
110
|
+
trace "Nothing from Master, Pinging"
|
111
|
+
@io.write Ping.new
|
112
|
+
end
|
113
|
+
rescue IOError => ex
|
114
|
+
trace "Worker lost Master"
|
115
|
+
Thread.exit
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def process_messages_from_runners
|
122
|
+
@runners.each do |r|
|
123
|
+
@listeners << Thread.new do
|
124
|
+
while @running
|
125
|
+
begin
|
126
|
+
message = r[:io].gets
|
127
|
+
if message and !message.class.to_s.index("Runner").nil?
|
128
|
+
trace "Received Message from Runner"
|
129
|
+
trace "\t#{message.inspect}"
|
130
|
+
message.handle(self, r)
|
131
|
+
end
|
132
|
+
rescue IOError => ex
|
133
|
+
trace "Worker lost Runner [#{r.inspect}]"
|
134
|
+
Thread.exit
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Get the next idle runner
|
142
|
+
def idle_runner #:nodoc:
|
143
|
+
idle_r = nil
|
144
|
+
while idle_r.nil?
|
145
|
+
idle_r = @runners.detect{|runner| runner[:idle]}
|
146
|
+
end
|
147
|
+
return idle_r
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
Given /^a target file$/ do
|
2
|
+
@target_file = File.expand_path(File.join(Dir.tmpdir, 'hydra_test.txt'))
|
3
|
+
end
|
4
|
+
|
5
|
+
Given /^an alternate target file$/ do
|
6
|
+
@target_file = File.expand_path(File.join(Dir.tmpdir, 'alternate_hydra_test.txt'))
|
7
|
+
end
|
8
|
+
|
9
|
+
When /^I write "([^\"]*)" to the file$/ do |text|
|
10
|
+
f = File.new(@target_file, 'w')
|
11
|
+
f.write text
|
12
|
+
f.flush
|
13
|
+
f.close
|
14
|
+
end
|
15
|
+
|
16
|
+
Then /^"([^\"]*)" should be written in the file$/ do |text|
|
17
|
+
f = File.new(@target_file, 'r')
|
18
|
+
raise 'Did not write to file' unless text == f.read
|
19
|
+
f.close
|
20
|
+
end
|
21
|
+
|
data/test/master_test.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
|
3
|
+
class MasterTest < Test::Unit::TestCase
|
4
|
+
context "with a file to test and a destination to verify" do
|
5
|
+
setup do
|
6
|
+
# avoid having other tests interfering with us
|
7
|
+
sleep(0.2)
|
8
|
+
FileUtils.rm_f(target_file)
|
9
|
+
end
|
10
|
+
|
11
|
+
teardown do
|
12
|
+
FileUtils.rm_f(target_file)
|
13
|
+
end
|
14
|
+
|
15
|
+
should "run a test" do
|
16
|
+
Hydra::Master.new(
|
17
|
+
:files => [test_file]
|
18
|
+
)
|
19
|
+
assert File.exists?(target_file)
|
20
|
+
assert_equal "HYDRA", File.read(target_file)
|
21
|
+
end
|
22
|
+
|
23
|
+
should "run a spec with pending examples" do
|
24
|
+
progress_bar = Hydra::Listener::ProgressBar.new(StringIO.new)
|
25
|
+
Hydra::Master.new(
|
26
|
+
:files => [rspec_file_with_pending],
|
27
|
+
:listeners => [progress_bar]
|
28
|
+
)
|
29
|
+
assert File.exists?(target_file)
|
30
|
+
assert_equal "HYDRA", File.read(target_file)
|
31
|
+
assert_equal false, progress_bar.instance_variable_get('@errors')
|
32
|
+
end
|
33
|
+
|
34
|
+
should "generate a report" do
|
35
|
+
Hydra::Master.new(:files => [test_file])
|
36
|
+
assert File.exists?(target_file)
|
37
|
+
assert_equal "HYDRA", File.read(target_file)
|
38
|
+
report_file = File.join(Dir.tmpdir, 'hydra_heuristics.yml')
|
39
|
+
assert File.exists?(report_file)
|
40
|
+
assert report = YAML.load_file(report_file)
|
41
|
+
assert_not_nil report[test_file]
|
42
|
+
end
|
43
|
+
|
44
|
+
should "run a test 6 times on 1 worker with 2 runners" do
|
45
|
+
Hydra::Master.new(
|
46
|
+
:files => [test_file]*6,
|
47
|
+
:workers => [ { :type => :local, :runners => 2 } ]
|
48
|
+
)
|
49
|
+
assert File.exists?(target_file)
|
50
|
+
assert_equal "HYDRA"*6, File.read(target_file)
|
51
|
+
end
|
52
|
+
|
53
|
+
# The test being run sleeps for 5 seconds. So, if this was run in
|
54
|
+
# series, it would take at least 50 seconds. This test ensures that
|
55
|
+
# in runs in less than that amount of time. Since there are 10
|
56
|
+
# runners to run the file 10 times, it should only take 5-10 seconds
|
57
|
+
# based on overhead.
|
58
|
+
should "run a slow test 10 times on 1 worker with 10 runners quickly" do
|
59
|
+
start = Time.now
|
60
|
+
Hydra::Master.new(
|
61
|
+
:files => [File.join(File.dirname(__FILE__), 'fixtures', 'slow.rb')]*10,
|
62
|
+
:workers => [
|
63
|
+
{ :type => :local, :runners => 10 }
|
64
|
+
]
|
65
|
+
)
|
66
|
+
finish = Time.now
|
67
|
+
assert (finish-start) < 30, "took #{finish-start} seconds"
|
68
|
+
end
|
69
|
+
|
70
|
+
should "run a slow test 10 times on 2 workers with 5 runners each quickly" do
|
71
|
+
start = Time.now
|
72
|
+
Hydra::Master.new(
|
73
|
+
:files => [File.join(File.dirname(__FILE__), 'fixtures', 'slow.rb')]*10,
|
74
|
+
:workers => [
|
75
|
+
{ :type => :local, :runners => 5 },
|
76
|
+
{ :type => :local, :runners => 5 }
|
77
|
+
]
|
78
|
+
)
|
79
|
+
finish = Time.now
|
80
|
+
assert (finish-start) < 15, "took #{finish-start} seconds"
|
81
|
+
end
|
82
|
+
|
83
|
+
should "run a test via ssh" do
|
84
|
+
Hydra::Master.new(
|
85
|
+
:files => [test_file],
|
86
|
+
:workers => [{
|
87
|
+
:type => :ssh,
|
88
|
+
:connect => 'localhost',
|
89
|
+
:directory => File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')),
|
90
|
+
:runners => 1
|
91
|
+
}]
|
92
|
+
)
|
93
|
+
assert File.exists?(target_file)
|
94
|
+
assert_equal "HYDRA", File.read(target_file)
|
95
|
+
end
|
96
|
+
|
97
|
+
should "run a test with config from a yaml file" do
|
98
|
+
Hydra::Master.new(
|
99
|
+
:files => [test_file],
|
100
|
+
:config => File.join(File.dirname(__FILE__), 'fixtures', 'config.yml')
|
101
|
+
)
|
102
|
+
assert File.exists?(target_file)
|
103
|
+
assert_equal "HYDRA", File.read(target_file)
|
104
|
+
end
|
105
|
+
|
106
|
+
should "synchronize a test file over ssh with rsync" do
|
107
|
+
local = File.join(Dir.tmpdir, 'hydra', 'local')
|
108
|
+
remote = File.join(Dir.tmpdir, 'hydra', 'remote')
|
109
|
+
sync_test = File.join(File.dirname(__FILE__), 'fixtures', 'sync_test.rb')
|
110
|
+
[local, remote].each{|f| FileUtils.rm_rf f; FileUtils.mkdir_p f}
|
111
|
+
|
112
|
+
# setup the folders:
|
113
|
+
# local:
|
114
|
+
# - test_a
|
115
|
+
# - test_c
|
116
|
+
# remote:
|
117
|
+
# - test_b
|
118
|
+
#
|
119
|
+
# add test_c to exludes
|
120
|
+
FileUtils.cp(sync_test, File.join(local, 'test_a.rb'))
|
121
|
+
FileUtils.cp(sync_test, File.join(local, 'test_c.rb'))
|
122
|
+
FileUtils.cp(sync_test, File.join(remote, 'test_b.rb'))
|
123
|
+
|
124
|
+
# ensure a is not on remote
|
125
|
+
assert !File.exists?(File.join(remote, 'test_a.rb')), "A should not be on remote"
|
126
|
+
# ensure c is not on remote
|
127
|
+
assert !File.exists?(File.join(remote, 'test_c.rb')), "C should not be on remote"
|
128
|
+
# ensure b is on remote
|
129
|
+
assert File.exists?(File.join(remote, 'test_b.rb')), "B should be on remote"
|
130
|
+
|
131
|
+
Hydra::Master.new(
|
132
|
+
:files => ['test_a.rb'],
|
133
|
+
:workers => [{
|
134
|
+
:type => :ssh,
|
135
|
+
:connect => 'localhost',
|
136
|
+
:directory => remote,
|
137
|
+
:runners => 1
|
138
|
+
}],
|
139
|
+
:sync => {
|
140
|
+
:directory => local,
|
141
|
+
:exclude => ['test_c.rb']
|
142
|
+
}
|
143
|
+
)
|
144
|
+
# ensure a is copied
|
145
|
+
assert File.exists?(File.join(remote, 'test_a.rb')), "A was not copied"
|
146
|
+
# ensure c is not copied
|
147
|
+
assert !File.exists?(File.join(remote, 'test_c.rb')), "C was copied, should be excluded"
|
148
|
+
# ensure b is deleted
|
149
|
+
assert !File.exists?(File.join(remote, 'test_b.rb')), "B was not deleted"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|