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.
- data/.document +5 -0
- data/.gitignore +22 -0
- data/LICENSE +20 -0
- data/README.rdoc +39 -0
- data/Rakefile +55 -0
- data/TODO +18 -0
- data/VERSION +1 -0
- data/caliper.yml +6 -0
- data/hydra-icon-64x64.png +0 -0
- data/hydra.gemspec +122 -0
- data/hydra_gray.png +0 -0
- data/lib/hydra/cucumber/formatter.rb +30 -0
- data/lib/hydra/hash.rb +16 -0
- data/lib/hydra/listener/abstract.rb +30 -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 +224 -0
- data/lib/hydra/message/master_messages.rb +19 -0
- data/lib/hydra/message/runner_messages.rb +46 -0
- data/lib/hydra/message/worker_messages.rb +46 -0
- data/lib/hydra/message.rb +47 -0
- data/lib/hydra/messaging_io.rb +48 -0
- data/lib/hydra/pipe.rb +61 -0
- data/lib/hydra/runner.rb +214 -0
- data/lib/hydra/safe_fork.rb +31 -0
- data/lib/hydra/spec/autorun_override.rb +12 -0
- data/lib/hydra/spec/hydra_formatter.rb +17 -0
- data/lib/hydra/ssh.rb +40 -0
- data/lib/hydra/stdio.rb +16 -0
- data/lib/hydra/sync.rb +99 -0
- data/lib/hydra/tasks.rb +256 -0
- data/lib/hydra/trace.rb +24 -0
- data/lib/hydra/worker.rb +146 -0
- data/lib/hydra.rb +16 -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/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 +144 -0
- data/test/ssh_test.rb +14 -0
- data/test/sync_test.rb +113 -0
- data/test/test_helper.rb +60 -0
- data/test/worker_test.rb +58 -0
- metadata +179 -0
data/lib/hydra/runner.rb
ADDED
@@ -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,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
|
data/lib/hydra/stdio.rb
ADDED
@@ -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
|
data/lib/hydra/tasks.rb
ADDED
@@ -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
|