sskirby-hydra 0.16.9

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.
Files changed (57) hide show
  1. data/.document +5 -0
  2. data/.gitignore +22 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +39 -0
  5. data/Rakefile +55 -0
  6. data/TODO +18 -0
  7. data/VERSION +1 -0
  8. data/caliper.yml +6 -0
  9. data/hydra-icon-64x64.png +0 -0
  10. data/hydra.gemspec +122 -0
  11. data/hydra_gray.png +0 -0
  12. data/lib/hydra/cucumber/formatter.rb +30 -0
  13. data/lib/hydra/hash.rb +16 -0
  14. data/lib/hydra/listener/abstract.rb +30 -0
  15. data/lib/hydra/listener/minimal_output.rb +24 -0
  16. data/lib/hydra/listener/notifier.rb +17 -0
  17. data/lib/hydra/listener/progress_bar.rb +48 -0
  18. data/lib/hydra/listener/report_generator.rb +30 -0
  19. data/lib/hydra/master.rb +224 -0
  20. data/lib/hydra/message/master_messages.rb +19 -0
  21. data/lib/hydra/message/runner_messages.rb +46 -0
  22. data/lib/hydra/message/worker_messages.rb +46 -0
  23. data/lib/hydra/message.rb +47 -0
  24. data/lib/hydra/messaging_io.rb +48 -0
  25. data/lib/hydra/pipe.rb +61 -0
  26. data/lib/hydra/runner.rb +214 -0
  27. data/lib/hydra/safe_fork.rb +31 -0
  28. data/lib/hydra/spec/autorun_override.rb +12 -0
  29. data/lib/hydra/spec/hydra_formatter.rb +17 -0
  30. data/lib/hydra/ssh.rb +40 -0
  31. data/lib/hydra/stdio.rb +16 -0
  32. data/lib/hydra/sync.rb +99 -0
  33. data/lib/hydra/tasks.rb +256 -0
  34. data/lib/hydra/trace.rb +24 -0
  35. data/lib/hydra/worker.rb +146 -0
  36. data/lib/hydra.rb +16 -0
  37. data/test/fixtures/assert_true.rb +7 -0
  38. data/test/fixtures/config.yml +4 -0
  39. data/test/fixtures/features/step_definitions.rb +21 -0
  40. data/test/fixtures/features/write_alternate_file.feature +7 -0
  41. data/test/fixtures/features/write_file.feature +7 -0
  42. data/test/fixtures/hello_world.rb +3 -0
  43. data/test/fixtures/slow.rb +9 -0
  44. data/test/fixtures/sync_test.rb +8 -0
  45. data/test/fixtures/write_file.rb +10 -0
  46. data/test/fixtures/write_file_alternate_spec.rb +10 -0
  47. data/test/fixtures/write_file_spec.rb +9 -0
  48. data/test/fixtures/write_file_with_pending_spec.rb +11 -0
  49. data/test/master_test.rb +152 -0
  50. data/test/message_test.rb +31 -0
  51. data/test/pipe_test.rb +38 -0
  52. data/test/runner_test.rb +144 -0
  53. data/test/ssh_test.rb +14 -0
  54. data/test/sync_test.rb +113 -0
  55. data/test/test_helper.rb +60 -0
  56. data/test/worker_test.rb +58 -0
  57. metadata +179 -0
@@ -0,0 +1,214 @@
1
+ require 'test/unit'
2
+ require 'test/unit/testresult'
3
+ Test::Unit.run = true
4
+
5
+ module Hydra #:nodoc:
6
+ # Hydra class responsible for running test files.
7
+ #
8
+ # The Runner is never run directly by a user. Runners are created by a
9
+ # Worker to run test files.
10
+ #
11
+ # The general convention is to have one Runner for each logical processor
12
+ # of a machine.
13
+ class Runner
14
+ include Hydra::Messages::Runner
15
+ traceable('RUNNER')
16
+ # Boot up a runner. It takes an IO object (generally a pipe from its
17
+ # parent) to send it messages on which files to execute.
18
+ def initialize(opts = {})
19
+ @io = opts.fetch(:io) { raise "No IO Object" }
20
+ @verbose = opts.fetch(:verbose) { false }
21
+ $stdout.sync = true
22
+ trace 'Booted. Sending Request for file'
23
+
24
+ @io.write RequestFile.new
25
+ begin
26
+ process_messages
27
+ rescue => ex
28
+ trace ex.to_s
29
+ raise ex
30
+ end
31
+ end
32
+
33
+ # Run a test file and report the results
34
+ def run_file(file)
35
+ trace "Running file: #{file}"
36
+
37
+ output = ""
38
+ if file =~ /_spec.rb$/
39
+ output = run_rspec_file(file)
40
+ elsif file =~ /.feature$/
41
+ output = run_cucumber_file(file)
42
+ else
43
+ output = run_test_unit_file(file)
44
+ end
45
+
46
+ output = "." if output == ""
47
+
48
+ @io.write Results.new(:output => output, :file => file)
49
+ return output
50
+ end
51
+
52
+ # Stop running
53
+ def stop
54
+ @running = false
55
+ end
56
+
57
+ private
58
+
59
+ # The runner will continually read messages and handle them.
60
+ def process_messages
61
+ trace "Processing Messages"
62
+ @running = true
63
+ while @running
64
+ begin
65
+ message = @io.gets
66
+ if message and !message.class.to_s.index("Worker").nil?
67
+ trace "Received message from worker"
68
+ trace "\t#{message.inspect}"
69
+ message.handle(self)
70
+ else
71
+ @io.write Ping.new
72
+ end
73
+ rescue IOError => ex
74
+ trace "Runner lost Worker"
75
+ @running = false
76
+ end
77
+ end
78
+ end
79
+
80
+ # Run all the Test::Unit Suites in a ruby file
81
+ def run_test_unit_file(file)
82
+ begin
83
+ require file
84
+ rescue LoadError => ex
85
+ trace "#{file} does not exist [#{ex.to_s}]"
86
+ return ex.to_s
87
+ end
88
+ output = []
89
+ @result = Test::Unit::TestResult.new
90
+ @result.add_listener(Test::Unit::TestResult::FAULT) do |value|
91
+ output << value
92
+ end
93
+
94
+ klasses = Runner.find_classes_in_file(file)
95
+ begin
96
+ klasses.each{|klass| klass.suite.run(@result){|status, name| ;}}
97
+ rescue => ex
98
+ output << ex.to_s
99
+ end
100
+
101
+ return output.join("\n")
102
+ end
103
+
104
+ # run all the Specs in an RSpec file (NOT IMPLEMENTED)
105
+ def run_rspec_file(file)
106
+ # pull in rspec
107
+ begin
108
+ require 'spec'
109
+ require 'hydra/spec/hydra_formatter'
110
+ # Ensure we override rspec's at_exit
111
+ require 'hydra/spec/autorun_override'
112
+ rescue LoadError => ex
113
+ return ex.to_s
114
+ end
115
+ hydra_output = StringIO.new
116
+ Spec::Runner.options.instance_variable_set(:@formatters, [
117
+ Spec::Runner::Formatter::HydraFormatter.new(
118
+ Spec::Runner.options.formatter_options,
119
+ hydra_output
120
+ )
121
+ ])
122
+ Spec::Runner.options.instance_variable_set(
123
+ :@example_groups, []
124
+ )
125
+ Spec::Runner.options.instance_variable_set(
126
+ :@files, [file]
127
+ )
128
+ Spec::Runner.options.instance_variable_set(
129
+ :@files_loaded, false
130
+ )
131
+ Spec::Runner.options.run_examples
132
+ hydra_output.rewind
133
+ output = hydra_output.read.chomp
134
+ output = "" if output.gsub("\n","") =~ /^\.*$/
135
+
136
+ return output
137
+ end
138
+
139
+ # run all the scenarios in a cucumber feature file
140
+ def run_cucumber_file(file)
141
+
142
+ files = [file]
143
+ dev_null = StringIO.new
144
+ hydra_response = StringIO.new
145
+
146
+ unless @step_mother
147
+ require 'cucumber'
148
+ require 'hydra/cucumber/formatter'
149
+ @step_mother = Cucumber::StepMother.new
150
+ @cuke_configuration = Cucumber::Cli::Configuration.new(dev_null, dev_null)
151
+ @cuke_configuration.parse!(['features']+files)
152
+
153
+ @step_mother.options = @cuke_configuration.options
154
+ @step_mother.log = @cuke_configuration.log
155
+ @step_mother.load_code_files(@cuke_configuration.support_to_load)
156
+ @step_mother.after_configuration(@cuke_configuration)
157
+ @step_mother.load_code_files(@cuke_configuration.step_defs_to_load)
158
+ end
159
+ cuke_formatter = Cucumber::Formatter::Hydra.new(
160
+ @step_mother, hydra_response, @cuke_configuration.options
161
+ )
162
+
163
+ cuke_runner ||= Cucumber::Ast::TreeWalker.new(
164
+ @step_mother, [cuke_formatter], @cuke_configuration.options, dev_null
165
+ )
166
+ @step_mother.visitor = cuke_runner
167
+
168
+ features = @step_mother.load_plain_text_features(files)
169
+ tag_excess = tag_excess(features, @cuke_configuration.options[:tag_expression].limits)
170
+ @cuke_configuration.options[:tag_excess] = tag_excess
171
+
172
+ cuke_runner.visit_features(features)
173
+
174
+ hydra_response.rewind
175
+ return hydra_response.read
176
+ end
177
+
178
+ # find all the test unit classes in a given file, so we can run their suites
179
+ def self.find_classes_in_file(f)
180
+ code = ""
181
+ File.open(f) {|buffer| code = buffer.read}
182
+ matches = code.scan(/class\s+([\S]+)/)
183
+ klasses = matches.collect do |c|
184
+ begin
185
+ if c.first.respond_to? :constantize
186
+ c.first.constantize
187
+ else
188
+ eval(c.first)
189
+ end
190
+ rescue NameError
191
+ # means we could not load [c.first], but thats ok, its just not
192
+ # one of the classes we want to test
193
+ nil
194
+ rescue SyntaxError
195
+ # see above
196
+ nil
197
+ end
198
+ end
199
+ return klasses.select{|k| k.respond_to? 'suite'}
200
+ end
201
+
202
+ # Yanked a method from Cucumber
203
+ def tag_excess(features, limits)
204
+ limits.map do |tag_name, tag_limit|
205
+ tag_locations = features.tag_locations(tag_name)
206
+ if tag_limit && (tag_locations.length > tag_limit)
207
+ [tag_name, tag_limit, tag_locations]
208
+ else
209
+ nil
210
+ end
211
+ end.compact
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,31 @@
1
+ class SafeFork
2
+ def self.fork
3
+ begin
4
+ # remove our connection so it doesn't get cloned
5
+ connection = ActiveRecord::Base.remove_connection if defined?(ActiveRecord)
6
+ # fork a process
7
+ child = Process.fork do
8
+ begin
9
+ # create a new connection and perform the action
10
+ begin
11
+ ActiveRecord::Base.establish_connection((connection || {}).merge({:allow_concurrency => true})) if defined?(ActiveRecord)
12
+ rescue ActiveRecord::AdapterNotSpecified
13
+ # AR was defined but we didn't have a connection
14
+ end
15
+ yield
16
+ ensure
17
+ # make sure we remove the connection before we're done
18
+ ActiveRecord::Base.remove_connection if defined?(ActiveRecord)
19
+ end
20
+ end
21
+ ensure
22
+ # make sure we re-establish the connection before returning to the main instance
23
+ begin
24
+ ActiveRecord::Base.establish_connection((connection || {}).merge({:allow_concurrency => true})) if defined?(ActiveRecord)
25
+ rescue ActiveRecord::AdapterNotSpecified
26
+ # AR was defined but we didn't have a connection
27
+ end
28
+ end
29
+ return child
30
+ end
31
+ end
@@ -0,0 +1,12 @@
1
+ if defined?(Spec)
2
+ module Spec
3
+ module Runner
4
+ class << self
5
+ # stop the auto-run at_exit
6
+ def run
7
+ return 0
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec/runner/formatter/progress_bar_formatter'
2
+ module Spec
3
+ module Runner
4
+ module Formatter
5
+ class HydraFormatter < ProgressBarFormatter
6
+ # Stifle the post-test summary
7
+ def dump_summary(duration, example, failure, pending)
8
+ end
9
+
10
+ # Stifle the output of pending examples
11
+ def example_pending(*args)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+
data/lib/hydra/ssh.rb ADDED
@@ -0,0 +1,40 @@
1
+ require 'open3'
2
+ require 'hydra/messaging_io'
3
+ module Hydra #:nodoc:
4
+ # Read and write with an ssh connection. For example:
5
+ # @ssh = Hydra::SSH.new(
6
+ # 'localhost', # connect to this machine
7
+ # '/home/user', # move to the home directory
8
+ # "ruby hydra/test/echo_the_dolphin.rb" # run the echo script
9
+ # )
10
+ # @message = Hydra::Messages::TestMessage.new("Hey there!")
11
+ # @ssh.write @message
12
+ # puts @ssh.gets.text
13
+ # => "Hey there!"
14
+ #
15
+ # Note that what ever process you run should respond with Hydra messages.
16
+ class SSH
17
+ include Open3
18
+ include Hydra::MessagingIO
19
+
20
+ # Initialize new SSH connection.
21
+ # The first parameter is passed directly to ssh for starting a connection.
22
+ # The second parameter is the directory to CD into once connected.
23
+ # The third parameter is the command to run
24
+ # So you can do:
25
+ # Hydra::SSH.new('-p 3022 user@server.com', '/home/user/Desktop', 'ls -l')
26
+ # To connect to server.com as user on port 3022, then CD to their desktop, then
27
+ # list all the files.
28
+ def initialize(connection_options, directory, command)
29
+ @writer, @reader, @error = popen3("ssh -tt #{connection_options}")
30
+ @writer.write("cd #{directory}\n")
31
+ @writer.write(command+"\n")
32
+ end
33
+
34
+ # Close the SSH connection
35
+ def close
36
+ @writer.write "exit\n"
37
+ super
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,16 @@
1
+ require 'hydra/messaging_io'
2
+ module Hydra #:nodoc:
3
+ # Read and write via stdout and stdin.
4
+ class Stdio
5
+ include Hydra::MessagingIO
6
+
7
+ # Initialize new Stdio
8
+ def initialize()
9
+ @reader = $stdin
10
+ @writer = $stdout
11
+ @reader.sync = true
12
+ @writer.sync = true
13
+ end
14
+ end
15
+ end
16
+
data/lib/hydra/sync.rb ADDED
@@ -0,0 +1,99 @@
1
+ require 'yaml'
2
+ module Hydra #:nodoc:
3
+ # Hydra class responsible for delegate work down to workers.
4
+ #
5
+ # The Sync is run once for each remote worker.
6
+ class Sync
7
+ traceable('SYNC')
8
+ self.class.traceable('SYNC MANY')
9
+
10
+ attr_reader :connect, :ssh_opts, :remote_dir
11
+
12
+ # Create a new Sync instance to rsync source from the local machine to a remote worker
13
+ #
14
+ # Arguments:
15
+ # * :worker_opts
16
+ # * A hash of the configuration options for a worker.
17
+ # * :sync
18
+ # * A hash of settings specifically for copying the source directory to be tested
19
+ # to the remote worked
20
+ # * :verbose
21
+ # * Set to true to see lots of Hydra output (for debugging)
22
+ def initialize(worker_opts, sync_opts, verbose = false)
23
+ worker_opts ||= {}
24
+ worker_opts.stringify_keys!
25
+ @verbose = verbose
26
+ @connect = worker_opts.fetch('connect') { raise "You must specify an SSH connection target" }
27
+ @ssh_opts = worker_opts.fetch('ssh_opts') { "" }
28
+ @remote_dir = worker_opts.fetch('directory') { raise "You must specify a remote directory" }
29
+
30
+ return unless sync_opts
31
+ sync_opts.stringify_keys!
32
+ @local_dir = sync_opts.fetch('directory') { raise "You must specify a synchronization directory" }
33
+ @exclude_paths = sync_opts.fetch('exclude') { [] }
34
+
35
+ trace "Initialized"
36
+ trace " Worker: (#{worker_opts.inspect})"
37
+ trace " Sync: (#{sync_opts.inspect})"
38
+
39
+ sync
40
+ end
41
+
42
+ def sync
43
+ #trace "Synchronizing with #{connect}\n\t#{sync_opts.inspect}"
44
+ exclude_opts = @exclude_paths.inject(''){|memo, path| memo += "--exclude=#{path} "}
45
+
46
+ rsync_command = [
47
+ 'rsync',
48
+ '-avz',
49
+ '--delete',
50
+ exclude_opts,
51
+ File.expand_path(@local_dir)+'/',
52
+ "-e \"ssh #{@ssh_opts}\"",
53
+ "#{@connect}:#{@remote_dir}"
54
+ ].join(" ")
55
+ trace rsync_command
56
+ trace `#{rsync_command}`
57
+ end
58
+
59
+ def self.sync_many opts
60
+ opts.stringify_keys!
61
+ config_file = opts.delete('config') { nil }
62
+ if config_file
63
+ opts.merge!(YAML.load_file(config_file).stringify_keys!)
64
+ end
65
+ @verbose = opts.fetch('verbose') { false }
66
+ @sync = opts.fetch('sync') { {} }
67
+
68
+ workers_opts = opts.fetch('workers') { [] }
69
+ @remote_worker_opts = []
70
+ workers_opts.each do |worker_opts|
71
+ worker_opts.stringify_keys!
72
+ if worker_opts['type'].to_s == 'ssh'
73
+ @remote_worker_opts << worker_opts
74
+ end
75
+ end
76
+
77
+ trace "Initialized"
78
+ trace " Sync: (#{@sync.inspect})"
79
+ trace " Workers: (#{@remote_worker_opts.inspect})"
80
+
81
+ Thread.abort_on_exception = true
82
+ trace "Processing workers"
83
+ @listeners = []
84
+ @remote_worker_opts.each do |worker_opts|
85
+ @listeners << Thread.new do
86
+ begin
87
+ trace "Syncing #{worker_opts.inspect}"
88
+ Sync.new worker_opts, @sync, @verbose
89
+ rescue
90
+ trace "Syncing failed [#{worker_opts.inspect}]"
91
+ end
92
+ end
93
+ end
94
+
95
+ @listeners.each{|l| l.join}
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,256 @@
1
+ require 'open3'
2
+ module Hydra #:nodoc:
3
+ # Hydra Task Common attributes and methods
4
+ class Task
5
+ # Name of the task. Default 'hydra'
6
+ attr_accessor :name
7
+
8
+ # Files to test.
9
+ # You can add files manually via:
10
+ # t.files << [file1, file2, etc]
11
+ #
12
+ # Or you can use the add_files method
13
+ attr_accessor :files
14
+
15
+ # True if you want to see Hydra's message traces
16
+ attr_accessor :verbose
17
+
18
+ # Path to the hydra config file.
19
+ # If not set, it will check 'hydra.yml' and 'config/hydra.yml'
20
+ attr_accessor :config
21
+
22
+ # Automatically sort files using their historical runtimes.
23
+ # Defaults to true
24
+ # To disable:
25
+ # t.autosort = false
26
+ attr_accessor :autosort
27
+
28
+ # Event listeners. Defaults to the MinimalOutput listener.
29
+ # You can add additional listeners if you'd like. For example,
30
+ # on linux (with notify-send) you can add the notifier listener:
31
+ # t.listeners << Hydra::Listener::Notifier.new
32
+ attr_accessor :listeners
33
+
34
+ #
35
+ # Search for the hydra config file
36
+ def find_config_file
37
+ @config ||= 'hydra.yml'
38
+ return @config if File.exists?(@config)
39
+ @config = File.join('config', 'hydra.yml')
40
+ return @config if File.exists?(@config)
41
+ @config = nil
42
+ end
43
+
44
+ # Add files to test by passing in a string to be run through Dir.glob.
45
+ # For example:
46
+ #
47
+ # t.add_files 'test/units/*.rb'
48
+ def add_files(pattern)
49
+ @files += Dir.glob(pattern)
50
+ end
51
+
52
+ end
53
+
54
+ # Define a test task that uses hydra to test the files.
55
+ #
56
+ # Hydra::TestTask.new('hydra') do |t|
57
+ # t.add_files 'test/unit/**/*_test.rb'
58
+ # t.add_files 'test/functional/**/*_test.rb'
59
+ # t.add_files 'test/integration/**/*_test.rb'
60
+ # t.verbose = false # optionally set to true for lots of debug messages
61
+ # t.autosort = false # disable automatic sorting based on runtime of tests
62
+ # end
63
+ class TestTask < Hydra::Task
64
+
65
+ # Create a new HydraTestTask
66
+ def initialize(name = :hydra)
67
+ @name = name
68
+ @files = []
69
+ @verbose = false
70
+ @autosort = true
71
+ @listeners = [Hydra::Listener::ProgressBar.new]
72
+
73
+ yield self if block_given?
74
+
75
+ # Ensure we override rspec's at_exit
76
+ require 'hydra/spec/autorun_override'
77
+
78
+ @config = find_config_file
79
+
80
+ @opts = {
81
+ :verbose => @verbose,
82
+ :autosort => @autosort,
83
+ :files => @files,
84
+ :listeners => @listeners
85
+ }
86
+ if @config
87
+ @opts.merge!(:config => @config)
88
+ else
89
+ @opts.merge!(:workers => [{:type => :local, :runners => 1}])
90
+ end
91
+
92
+ define
93
+ end
94
+
95
+ private
96
+ # Create the rake task defined by this HydraTestTask
97
+ def define
98
+ desc "Hydra Tests" + (@name == :hydra ? "" : " for #{@name}")
99
+ task @name do
100
+ Hydra::Master.new(@opts)
101
+ #exit(0) #bypass test on_exit output
102
+ end
103
+ end
104
+ end
105
+
106
+ # Define a sync task that uses hydra to rsync the source tree under test to remote workers.
107
+ #
108
+ # This task is very useful to run before a remote db:reset task to make sure the db/schema.rb
109
+ # file is up to date on the remote workers.
110
+ #
111
+ # Hydra::SyncTask.new('hydra:sync') do |t|
112
+ # t.verbose = false # optionally set to true for lots of debug messages
113
+ # end
114
+ class SyncTask < Hydra::Task
115
+
116
+ # Create a new SyncTestTask
117
+ def initialize(name = :sync)
118
+ @name = name
119
+ @verbose = false
120
+
121
+ yield self if block_given?
122
+
123
+ @config = find_config_file
124
+
125
+ @opts = {
126
+ :verbose => @verbose
127
+ }
128
+ @opts.merge!(:config => @config) if @config
129
+
130
+ define
131
+ end
132
+
133
+ private
134
+ # Create the rake task defined by this HydraSyncTask
135
+ def define
136
+ desc "Hydra Tests" + (@name == :hydra ? "" : " for #{@name}")
137
+ task @name do
138
+ Hydra::Sync.sync_many(@opts)
139
+ end
140
+ end
141
+ end
142
+
143
+ # Setup a task that will be run across all remote workers
144
+ # Hydra::RemoteTask.new('db:reset')
145
+ #
146
+ # Then you can run:
147
+ # rake hydra:remote:db:reset
148
+ class RemoteTask < Hydra::Task
149
+ include Open3
150
+ # Create a new hydra remote task with the given name.
151
+ # The task will be named hydra:remote:<name>
152
+ def initialize(name)
153
+ @name = name
154
+ yield self if block_given?
155
+ @config = find_config_file
156
+ if @config
157
+ define
158
+ else
159
+ task "hydra:remote:#{@name}" do ; end
160
+ end
161
+ end
162
+
163
+ private
164
+ def define
165
+ desc "Run #{@name} remotely on all workers"
166
+ task "hydra:remote:#{@name}" do
167
+ config = YAML.load_file(@config)
168
+ environment = config.fetch('environment') { 'test' }
169
+ workers = config.fetch('workers') { [] }
170
+ workers = workers.select{|w| w['type'] == 'ssh'}
171
+
172
+ $stdout.write "==== Hydra Running #{@name} ====\n"
173
+ Thread.abort_on_exception = true
174
+ @listeners = []
175
+ @results = {}
176
+ workers.each do |worker|
177
+ @listeners << Thread.new do
178
+ begin
179
+ @results[worker] = if run_task(worker, environment)
180
+ "==== #{@name} passed on #{worker['connect']} ====\n"
181
+ else
182
+ "==== #{@name} failed on #{worker['connect']} ====\nPlease see above for more details.\n"
183
+ end
184
+ rescue
185
+ @results[worker] = "==== #{@name} failed for #{worker['connect']} ====\n#{$!.inspect}\n#{$!.backtrace.join("\n")}"
186
+ end
187
+ end
188
+ end
189
+ @listeners.each{|l| l.join}
190
+ $stdout.write "\n==== Hydra Running #{@name} COMPLETE ====\n\n"
191
+ $stdout.write @results.values.join("\n")
192
+ end
193
+ end
194
+
195
+ def run_task worker, environment
196
+ $stdout.write "==== Hydra Running #{@name} on #{worker['connect']} ====\n"
197
+ ssh_opts = worker.fetch('ssh_opts') { '' }
198
+ writer, reader, error = popen3("ssh -tt #{ssh_opts} #{worker['connect']} ")
199
+ writer.write("cd #{worker['directory']}\n")
200
+ writer.write "echo BEGIN HYDRA\n"
201
+ writer.write("RAILS_ENV=#{environment} rake #{@name}\n")
202
+ writer.write "echo END HYDRA\n"
203
+ writer.write("exit\n")
204
+ writer.close
205
+ ignoring = true
206
+ passed = true
207
+ while line = reader.gets
208
+ line.chomp!
209
+ if line =~ /^rake aborted!$/
210
+ passed = false
211
+ end
212
+ if line =~ /echo END HYDRA$/
213
+ ignoring = true
214
+ end
215
+ $stdout.write "#{worker['connect']}: #{line}\n" unless ignoring
216
+ if line == 'BEGIN HYDRA'
217
+ ignoring = false
218
+ end
219
+ end
220
+ passed
221
+ end
222
+ end
223
+
224
+ # A Hydra global task is a task that is run both locally and remotely.
225
+ #
226
+ # For example:
227
+ #
228
+ # Hydra::GlobalTask.new('db:reset')
229
+ #
230
+ # Allows you to run:
231
+ #
232
+ # rake hydra:db:reset
233
+ #
234
+ # Then, db:reset will be run locally and on all remote workers. This
235
+ # makes it easy to setup your workers and run tasks all in a row.
236
+ #
237
+ # For example:
238
+ #
239
+ # rake hydra:db:reset hydra:factories hydra:tests
240
+ #
241
+ # Assuming you setup hydra:db:reset and hydra:db:factories as global
242
+ # tasks and hydra:tests as a Hydra::TestTask for all your tests
243
+ class GlobalTask < Hydra::Task
244
+ def initialize(name)
245
+ @name = name
246
+ define
247
+ end
248
+
249
+ private
250
+ def define
251
+ Hydra::RemoteTask.new(@name)
252
+ desc "Run #{@name.to_s} Locally and Remotely across all Workers"
253
+ task "hydra:#{@name.to_s}" => [@name.to_s, "hydra:remote:#{@name.to_s}"]
254
+ end
255
+ end
256
+ end