nulogy-hydra 0.23.2.1
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/LICENSE +20 -0
- data/README.rdoc +39 -0
- data/Rakefile +56 -0
- data/TODO +18 -0
- data/VERSION +1 -0
- data/caliper.yml +6 -0
- data/hydra-icon-64x64.png +0 -0
- data/hydra.gemspec +131 -0
- data/hydra_gray.png +0 -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 +31 -0
- data/lib/hydra/master.rb +252 -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 +52 -0
- data/lib/hydra/message.rb +47 -0
- data/lib/hydra/messaging_io.rb +49 -0
- data/lib/hydra/pipe.rb +61 -0
- data/lib/hydra/proxy_config.rb +27 -0
- data/lib/hydra/runner.rb +306 -0
- data/lib/hydra/runner_listener/abstract.rb +23 -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 +366 -0
- data/lib/hydra/threadsafe_io.rb +18 -0
- data/lib/hydra/tmpdir.rb +11 -0
- data/lib/hydra/trace.rb +29 -0
- data/lib/hydra/worker.rb +168 -0
- data/lib/hydra.rb +16 -0
- data/nulogy-hydra.gemspec +122 -0
- data/test/fixtures/assert_true.rb +7 -0
- data/test/fixtures/bad_proxy_config.yml +4 -0
- data/test/fixtures/config.yml +4 -0
- data/test/fixtures/conflicting.rb +10 -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/hydra_worker_init.rb +2 -0
- data/test/fixtures/js_file.js +4 -0
- data/test/fixtures/json_data.json +4 -0
- data/test/fixtures/many_outputs_to_console.rb +9 -0
- data/test/fixtures/master_listeners.rb +10 -0
- data/test/fixtures/proxy_config.yml +4 -0
- data/test/fixtures/proxy_config_http.yml +4 -0
- data/test/fixtures/runner_listeners.rb +23 -0
- data/test/fixtures/slow.rb +9 -0
- data/test/fixtures/sync_test.rb +8 -0
- data/test/fixtures/task_test_config.yml +6 -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 +383 -0
- data/test/message_test.rb +31 -0
- data/test/pipe_test.rb +38 -0
- data/test/proxy_config_test.rb +31 -0
- data/test/runner_test.rb +196 -0
- data/test/ssh_test.rb +25 -0
- data/test/sync_test.rb +113 -0
- data/test/task_test.rb +21 -0
- data/test/test_helper.rb +107 -0
- data/test/worker_test.rb +60 -0
- metadata +208 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'net/http'
|
3
|
+
|
4
|
+
module Hydra
|
5
|
+
class UnknownProxyType < RuntimeError
|
6
|
+
|
7
|
+
end
|
8
|
+
|
9
|
+
class ProxyConfig
|
10
|
+
def self.load(config_yml)
|
11
|
+
config = YAML::load(config_yml)
|
12
|
+
if config.has_key?('proxy')
|
13
|
+
proxy_info = config['proxy']
|
14
|
+
#only file supported so far
|
15
|
+
if proxy_info['type'] == "file"
|
16
|
+
YAML::load_file(proxy_info['path'])
|
17
|
+
elsif proxy_info['type'] == "http"
|
18
|
+
YAML::load(Net::HTTP.get(URI.parse(proxy_info['path'])))
|
19
|
+
else
|
20
|
+
raise UnknownProxyType.new("Proxy config file does not know what to do with type '#{proxy_info["type"]}'.")
|
21
|
+
end
|
22
|
+
else
|
23
|
+
config
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/hydra/runner.rb
ADDED
@@ -0,0 +1,306 @@
|
|
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
|
+
|
17
|
+
DEFAULT_LOG_FILE = 'hydra-runner.log'
|
18
|
+
|
19
|
+
# Boot up a runner. It takes an IO object (generally a pipe from its
|
20
|
+
# parent) to send it messages on which files to execute.
|
21
|
+
def initialize(opts = {})
|
22
|
+
redirect_output( opts.fetch( :runner_log_file ) { DEFAULT_LOG_FILE } )
|
23
|
+
reg_trap_sighup
|
24
|
+
|
25
|
+
@io = opts.fetch(:io) { raise "No IO Object" }
|
26
|
+
@verbose = opts.fetch(:verbose) { false }
|
27
|
+
@event_listeners = Array( opts.fetch( :runner_listeners ) { nil } )
|
28
|
+
|
29
|
+
$stdout.sync = true
|
30
|
+
runner_begin
|
31
|
+
|
32
|
+
trace 'Booted. Sending Request for file'
|
33
|
+
@io.write RequestFile.new
|
34
|
+
begin
|
35
|
+
process_messages
|
36
|
+
rescue => ex
|
37
|
+
trace ex.to_s
|
38
|
+
raise ex
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def reg_trap_sighup
|
43
|
+
for sign in [:SIGHUP, :INT]
|
44
|
+
trap sign do
|
45
|
+
stop
|
46
|
+
end
|
47
|
+
end
|
48
|
+
@runner_began = true
|
49
|
+
end
|
50
|
+
|
51
|
+
def runner_begin
|
52
|
+
trace "Firing runner_begin event"
|
53
|
+
@event_listeners.each {|l| l.runner_begin( self ) }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Run a test file and report the results
|
57
|
+
def run_file(file)
|
58
|
+
trace "Running file: #{file}"
|
59
|
+
|
60
|
+
output = ""
|
61
|
+
if file =~ /_spec.rb$/i
|
62
|
+
output = run_rspec_file(file)
|
63
|
+
elsif file =~ /.feature$/i
|
64
|
+
output = run_cucumber_file(file)
|
65
|
+
elsif file =~ /.js$/i or file =~ /.json$/i
|
66
|
+
output = run_javascript_file(file)
|
67
|
+
else
|
68
|
+
output = run_test_unit_file(file)
|
69
|
+
end
|
70
|
+
|
71
|
+
output = "." if output == ""
|
72
|
+
|
73
|
+
@io.write Results.new(:output => output, :file => file)
|
74
|
+
return output
|
75
|
+
end
|
76
|
+
|
77
|
+
# Stop running
|
78
|
+
def stop
|
79
|
+
runner_end if @runner_began
|
80
|
+
@runner_began = @running = false
|
81
|
+
end
|
82
|
+
|
83
|
+
def runner_end
|
84
|
+
trace "Ending runner #{self.inspect}"
|
85
|
+
@event_listeners.each {|l| l.runner_end( self ) }
|
86
|
+
end
|
87
|
+
|
88
|
+
def format_exception(ex)
|
89
|
+
"#{ex.class.name}: #{ex.message}\n #{ex.backtrace.join("\n ")}"
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
# The runner will continually read messages and handle them.
|
95
|
+
def process_messages
|
96
|
+
trace "Processing Messages"
|
97
|
+
@running = true
|
98
|
+
while @running
|
99
|
+
begin
|
100
|
+
message = @io.gets
|
101
|
+
if message and !message.class.to_s.index("Worker").nil?
|
102
|
+
trace "Received message from worker"
|
103
|
+
trace "\t#{message.inspect}"
|
104
|
+
message.handle(self)
|
105
|
+
else
|
106
|
+
@io.write Ping.new
|
107
|
+
end
|
108
|
+
rescue IOError => ex
|
109
|
+
trace "Runner lost Worker"
|
110
|
+
stop
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def format_ex_in_file(file, ex)
|
116
|
+
"Error in #{file}:\n #{format_exception(ex)}"
|
117
|
+
end
|
118
|
+
|
119
|
+
# Run all the Test::Unit Suites in a ruby file
|
120
|
+
def run_test_unit_file(file)
|
121
|
+
begin
|
122
|
+
require file
|
123
|
+
rescue LoadError => ex
|
124
|
+
trace "#{file} does not exist [#{ex.to_s}]"
|
125
|
+
return ex.to_s
|
126
|
+
rescue Exception => ex
|
127
|
+
trace "Error requiring #{file} [#{ex.to_s}]"
|
128
|
+
return format_ex_in_file(file, ex)
|
129
|
+
end
|
130
|
+
output = []
|
131
|
+
@result = Test::Unit::TestResult.new
|
132
|
+
@result.add_listener(Test::Unit::TestResult::FAULT) do |value|
|
133
|
+
output << value
|
134
|
+
end
|
135
|
+
|
136
|
+
klasses = Runner.find_classes_in_file(file)
|
137
|
+
begin
|
138
|
+
klasses.each{|klass| klass.suite.run(@result){|status, name| ;}}
|
139
|
+
rescue => ex
|
140
|
+
output << format_ex_in_file(file, ex)
|
141
|
+
end
|
142
|
+
|
143
|
+
return output.join("\n")
|
144
|
+
end
|
145
|
+
|
146
|
+
# run all the Specs in an RSpec file (NOT IMPLEMENTED)
|
147
|
+
def run_rspec_file(file)
|
148
|
+
# pull in rspec
|
149
|
+
begin
|
150
|
+
require 'rspec'
|
151
|
+
require 'hydra/spec/hydra_formatter'
|
152
|
+
# Ensure we override rspec's at_exit
|
153
|
+
RSpec::Core::Runner.disable_autorun!
|
154
|
+
rescue LoadError => ex
|
155
|
+
return ex.to_s
|
156
|
+
end
|
157
|
+
hydra_output = StringIO.new
|
158
|
+
|
159
|
+
config = [
|
160
|
+
'-f', 'RSpec::Core::Formatters::HydraFormatter',
|
161
|
+
file
|
162
|
+
]
|
163
|
+
|
164
|
+
RSpec.instance_variable_set(:@world, nil)
|
165
|
+
RSpec::Core::Runner.run(config, hydra_output, hydra_output)
|
166
|
+
|
167
|
+
hydra_output.rewind
|
168
|
+
output = hydra_output.read.chomp
|
169
|
+
output = "" if output.gsub("\n","") =~ /^\.*$/
|
170
|
+
|
171
|
+
return output
|
172
|
+
end
|
173
|
+
|
174
|
+
# run all the scenarios in a cucumber feature file
|
175
|
+
def run_cucumber_file(file)
|
176
|
+
|
177
|
+
files = [file]
|
178
|
+
dev_null = StringIO.new
|
179
|
+
hydra_response = StringIO.new
|
180
|
+
|
181
|
+
unless @cuke_runtime
|
182
|
+
require 'cucumber'
|
183
|
+
require 'hydra/cucumber/formatter'
|
184
|
+
Cucumber.logger.level = Logger::INFO
|
185
|
+
@cuke_runtime = Cucumber::Runtime.new
|
186
|
+
@cuke_configuration = Cucumber::Cli::Configuration.new(dev_null, dev_null)
|
187
|
+
@cuke_configuration.parse!(ENV['CUCUMBER_OPTS'].split(' ') + ['features'] + files)
|
188
|
+
|
189
|
+
support_code = Cucumber::Runtime::SupportCode.new(@cuke_runtime, @cuke_configuration.guess?)
|
190
|
+
support_code.load_files!(@cuke_configuration.support_to_load + @cuke_configuration.step_defs_to_load)
|
191
|
+
support_code.fire_hook(:after_configuration, @cuke_configuration)
|
192
|
+
# i don't like this, but there no access to set the instance of SupportCode in Runtime
|
193
|
+
@cuke_runtime.instance_variable_set('@support_code',support_code)
|
194
|
+
end
|
195
|
+
cuke_formatter = Cucumber::Formatter::Hydra.new(
|
196
|
+
@cuke_runtime, hydra_response, @cuke_configuration.options
|
197
|
+
)
|
198
|
+
|
199
|
+
cuke_runner ||= Cucumber::Ast::TreeWalker.new(
|
200
|
+
@cuke_runtime, [cuke_formatter], @cuke_configuration
|
201
|
+
)
|
202
|
+
@cuke_runtime.visitor = cuke_runner
|
203
|
+
|
204
|
+
loader = Cucumber::Runtime::FeaturesLoader.new(
|
205
|
+
files,
|
206
|
+
@cuke_configuration.filters,
|
207
|
+
@cuke_configuration.tag_expression
|
208
|
+
)
|
209
|
+
features = loader.features
|
210
|
+
tag_excess = tag_excess(features, @cuke_configuration.options[:tag_expression].limits)
|
211
|
+
@cuke_configuration.options[:tag_excess] = tag_excess
|
212
|
+
|
213
|
+
cuke_runner.visit_features(features)
|
214
|
+
|
215
|
+
hydra_response.rewind
|
216
|
+
return hydra_response.read
|
217
|
+
end
|
218
|
+
|
219
|
+
def run_javascript_file(file)
|
220
|
+
errors = []
|
221
|
+
require 'v8'
|
222
|
+
V8::Context.new do |context|
|
223
|
+
context.load(File.expand_path(File.join(File.dirname(__FILE__), 'js', 'lint.js')))
|
224
|
+
context['input'] = lambda{
|
225
|
+
File.read(file)
|
226
|
+
}
|
227
|
+
context['reportErrors'] = lambda{|js_errors|
|
228
|
+
js_errors.each do |e|
|
229
|
+
e = V8::To.rb(e)
|
230
|
+
errors << "\n\e[1;31mJSLINT: #{file}\e[0m"
|
231
|
+
errors << " Error at line #{e['line'].to_i + 1} " +
|
232
|
+
"character #{e['character'].to_i + 1}: \e[1;33m#{e['reason']}\e[0m"
|
233
|
+
errors << "#{e['evidence']}"
|
234
|
+
end
|
235
|
+
}
|
236
|
+
context.eval %{
|
237
|
+
JSLINT(input(), {
|
238
|
+
sub: true,
|
239
|
+
onevar: true,
|
240
|
+
eqeqeq: true,
|
241
|
+
plusplus: true,
|
242
|
+
bitwise: true,
|
243
|
+
regexp: true,
|
244
|
+
newcap: true,
|
245
|
+
immed: true,
|
246
|
+
strict: true,
|
247
|
+
rhino: true
|
248
|
+
});
|
249
|
+
reportErrors(JSLINT.errors);
|
250
|
+
}
|
251
|
+
end
|
252
|
+
|
253
|
+
if errors.empty?
|
254
|
+
return '.'
|
255
|
+
else
|
256
|
+
return errors.join("\n")
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# find all the test unit classes in a given file, so we can run their suites
|
261
|
+
def self.find_classes_in_file(f)
|
262
|
+
code = ""
|
263
|
+
File.open(f) {|buffer| code = buffer.read}
|
264
|
+
matches = code.scan(/class\s+([\S]+)/)
|
265
|
+
klasses = matches.collect do |c|
|
266
|
+
begin
|
267
|
+
if c.first.respond_to? :constantize
|
268
|
+
c.first.constantize
|
269
|
+
else
|
270
|
+
eval(c.first)
|
271
|
+
end
|
272
|
+
rescue NameError
|
273
|
+
# means we could not load [c.first], but thats ok, its just not
|
274
|
+
# one of the classes we want to test
|
275
|
+
nil
|
276
|
+
rescue SyntaxError
|
277
|
+
# see above
|
278
|
+
nil
|
279
|
+
end
|
280
|
+
end
|
281
|
+
return klasses.select{|k| k.respond_to? 'suite'}
|
282
|
+
end
|
283
|
+
|
284
|
+
# Yanked a method from Cucumber
|
285
|
+
def tag_excess(features, limits)
|
286
|
+
limits.map do |tag_name, tag_limit|
|
287
|
+
tag_locations = features.tag_locations(tag_name)
|
288
|
+
if tag_limit && (tag_locations.length > tag_limit)
|
289
|
+
[tag_name, tag_limit, tag_locations]
|
290
|
+
else
|
291
|
+
nil
|
292
|
+
end
|
293
|
+
end.compact
|
294
|
+
end
|
295
|
+
|
296
|
+
def redirect_output file_name
|
297
|
+
begin
|
298
|
+
$stderr = $stdout = File.open(file_name, 'a')
|
299
|
+
rescue
|
300
|
+
# it should always redirect output in order to handle unexpected interruption
|
301
|
+
# successfully
|
302
|
+
$stderr = $stdout = File.open(DEFAULT_LOG_FILE, 'a')
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Hydra #:nodoc:
|
2
|
+
module RunnerListener #: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
|
+
|
14
|
+
# Fired by the runner just before requesting the first file
|
15
|
+
def runner_begin( runner )
|
16
|
+
end
|
17
|
+
|
18
|
+
# Fired by the runner just after stoping
|
19
|
+
def runner_end( runner )
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
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,26 @@
|
|
1
|
+
require 'rspec/core/formatters/progress_formatter'
|
2
|
+
module RSpec
|
3
|
+
module Core
|
4
|
+
module Formatters
|
5
|
+
class HydraFormatter < ProgressFormatter
|
6
|
+
def example_passed(example)
|
7
|
+
end
|
8
|
+
|
9
|
+
def example_pending(example)
|
10
|
+
end
|
11
|
+
|
12
|
+
def example_failed(example)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Stifle the post-test summary
|
16
|
+
def dump_summary(duration, example, failure, pending)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Stifle pending specs
|
20
|
+
def dump_pending
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
data/lib/hydra/ssh.rb
ADDED
@@ -0,0 +1,41 @@
|
|
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("mkdir -p #{directory}\n")
|
31
|
+
@writer.write("cd #{directory}\n")
|
32
|
+
@writer.write(command+"\n")
|
33
|
+
end
|
34
|
+
|
35
|
+
# Close the SSH connection
|
36
|
+
def close
|
37
|
+
@writer.write "exit\n"
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
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!(ProxyConfig.load(IO.read(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
|