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.
Files changed (75) hide show
  1. data/.document +5 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +39 -0
  4. data/Rakefile +56 -0
  5. data/TODO +18 -0
  6. data/VERSION +1 -0
  7. data/caliper.yml +6 -0
  8. data/hydra-icon-64x64.png +0 -0
  9. data/hydra.gemspec +131 -0
  10. data/hydra_gray.png +0 -0
  11. data/lib/hydra/cucumber/formatter.rb +29 -0
  12. data/lib/hydra/hash.rb +16 -0
  13. data/lib/hydra/js/lint.js +5150 -0
  14. data/lib/hydra/listener/abstract.rb +39 -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 +31 -0
  19. data/lib/hydra/master.rb +252 -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 +52 -0
  23. data/lib/hydra/message.rb +47 -0
  24. data/lib/hydra/messaging_io.rb +49 -0
  25. data/lib/hydra/pipe.rb +61 -0
  26. data/lib/hydra/proxy_config.rb +27 -0
  27. data/lib/hydra/runner.rb +306 -0
  28. data/lib/hydra/runner_listener/abstract.rb +23 -0
  29. data/lib/hydra/safe_fork.rb +31 -0
  30. data/lib/hydra/spec/autorun_override.rb +3 -0
  31. data/lib/hydra/spec/hydra_formatter.rb +26 -0
  32. data/lib/hydra/ssh.rb +41 -0
  33. data/lib/hydra/stdio.rb +16 -0
  34. data/lib/hydra/sync.rb +99 -0
  35. data/lib/hydra/tasks.rb +366 -0
  36. data/lib/hydra/threadsafe_io.rb +18 -0
  37. data/lib/hydra/tmpdir.rb +11 -0
  38. data/lib/hydra/trace.rb +29 -0
  39. data/lib/hydra/worker.rb +168 -0
  40. data/lib/hydra.rb +16 -0
  41. data/nulogy-hydra.gemspec +122 -0
  42. data/test/fixtures/assert_true.rb +7 -0
  43. data/test/fixtures/bad_proxy_config.yml +4 -0
  44. data/test/fixtures/config.yml +4 -0
  45. data/test/fixtures/conflicting.rb +10 -0
  46. data/test/fixtures/features/step_definitions.rb +21 -0
  47. data/test/fixtures/features/write_alternate_file.feature +7 -0
  48. data/test/fixtures/features/write_file.feature +7 -0
  49. data/test/fixtures/hello_world.rb +3 -0
  50. data/test/fixtures/hydra_worker_init.rb +2 -0
  51. data/test/fixtures/js_file.js +4 -0
  52. data/test/fixtures/json_data.json +4 -0
  53. data/test/fixtures/many_outputs_to_console.rb +9 -0
  54. data/test/fixtures/master_listeners.rb +10 -0
  55. data/test/fixtures/proxy_config.yml +4 -0
  56. data/test/fixtures/proxy_config_http.yml +4 -0
  57. data/test/fixtures/runner_listeners.rb +23 -0
  58. data/test/fixtures/slow.rb +9 -0
  59. data/test/fixtures/sync_test.rb +8 -0
  60. data/test/fixtures/task_test_config.yml +6 -0
  61. data/test/fixtures/write_file.rb +10 -0
  62. data/test/fixtures/write_file_alternate_spec.rb +10 -0
  63. data/test/fixtures/write_file_spec.rb +9 -0
  64. data/test/fixtures/write_file_with_pending_spec.rb +11 -0
  65. data/test/master_test.rb +383 -0
  66. data/test/message_test.rb +31 -0
  67. data/test/pipe_test.rb +38 -0
  68. data/test/proxy_config_test.rb +31 -0
  69. data/test/runner_test.rb +196 -0
  70. data/test/ssh_test.rb +25 -0
  71. data/test/sync_test.rb +113 -0
  72. data/test/task_test.rb +21 -0
  73. data/test/test_helper.rb +107 -0
  74. data/test/worker_test.rb +60 -0
  75. 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
@@ -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,3 @@
1
+ if defined?(RSpec)
2
+ RSpec::Core::Runner.disable_autorun!
3
+ 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
@@ -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