sskirby-hydra 0.16.9

Sign up to get free protection for your applications and to get access to all the features.
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