causes-hydra 0.21.0

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 (61) 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 +56 -0
  6. data/TODO +18 -0
  7. data/VERSION +1 -0
  8. data/bin/warmsnake.rb +76 -0
  9. data/caliper.yml +6 -0
  10. data/hydra-icon-64x64.png +0 -0
  11. data/hydra.gemspec +130 -0
  12. data/hydra_gray.png +0 -0
  13. data/lib/hydra.rb +16 -0
  14. data/lib/hydra/cucumber/formatter.rb +29 -0
  15. data/lib/hydra/hash.rb +16 -0
  16. data/lib/hydra/js/lint.js +5150 -0
  17. data/lib/hydra/listener/abstract.rb +39 -0
  18. data/lib/hydra/listener/minimal_output.rb +24 -0
  19. data/lib/hydra/listener/notifier.rb +17 -0
  20. data/lib/hydra/listener/progress_bar.rb +48 -0
  21. data/lib/hydra/listener/report_generator.rb +30 -0
  22. data/lib/hydra/master.rb +249 -0
  23. data/lib/hydra/message.rb +47 -0
  24. data/lib/hydra/message/master_messages.rb +19 -0
  25. data/lib/hydra/message/runner_messages.rb +52 -0
  26. data/lib/hydra/message/worker_messages.rb +52 -0
  27. data/lib/hydra/messaging_io.rb +46 -0
  28. data/lib/hydra/pipe.rb +61 -0
  29. data/lib/hydra/runner.rb +305 -0
  30. data/lib/hydra/safe_fork.rb +31 -0
  31. data/lib/hydra/spec/autorun_override.rb +3 -0
  32. data/lib/hydra/spec/hydra_formatter.rb +26 -0
  33. data/lib/hydra/ssh.rb +41 -0
  34. data/lib/hydra/stdio.rb +16 -0
  35. data/lib/hydra/sync.rb +99 -0
  36. data/lib/hydra/tasks.rb +342 -0
  37. data/lib/hydra/trace.rb +24 -0
  38. data/lib/hydra/worker.rb +150 -0
  39. data/test/fixtures/assert_true.rb +7 -0
  40. data/test/fixtures/config.yml +4 -0
  41. data/test/fixtures/features/step_definitions.rb +21 -0
  42. data/test/fixtures/features/write_alternate_file.feature +7 -0
  43. data/test/fixtures/features/write_file.feature +7 -0
  44. data/test/fixtures/hello_world.rb +3 -0
  45. data/test/fixtures/js_file.js +4 -0
  46. data/test/fixtures/json_data.json +4 -0
  47. data/test/fixtures/slow.rb +9 -0
  48. data/test/fixtures/sync_test.rb +8 -0
  49. data/test/fixtures/write_file.rb +10 -0
  50. data/test/fixtures/write_file_alternate_spec.rb +10 -0
  51. data/test/fixtures/write_file_spec.rb +9 -0
  52. data/test/fixtures/write_file_with_pending_spec.rb +11 -0
  53. data/test/master_test.rb +152 -0
  54. data/test/message_test.rb +31 -0
  55. data/test/pipe_test.rb +38 -0
  56. data/test/runner_test.rb +153 -0
  57. data/test/ssh_test.rb +14 -0
  58. data/test/sync_test.rb +113 -0
  59. data/test/test_helper.rb +68 -0
  60. data/test/worker_test.rb +60 -0
  61. metadata +209 -0
@@ -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
+
@@ -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
+
@@ -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,342 @@
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
+ # Set to true if you want to run this task only on the local
35
+ # machine with one runner. A "Safe Mode" for some test
36
+ # files that may not play nice with others.
37
+ attr_accessor :serial
38
+
39
+ #
40
+ # Search for the hydra config file
41
+ def find_config_file
42
+ @config ||= 'hydra.yml'
43
+ return @config if File.exists?(@config)
44
+ @config = File.join('config', 'hydra.yml')
45
+ return @config if File.exists?(@config)
46
+ @config = nil
47
+ end
48
+
49
+ # Add files to test by passing in a string to be run through Dir.glob.
50
+ # For example:
51
+ #
52
+ # t.add_files 'test/units/*.rb'
53
+ def add_files(pattern)
54
+ @files += Dir.glob(pattern)
55
+ end
56
+
57
+ end
58
+
59
+ # Define a test task that uses hydra to test the files.
60
+ #
61
+ # Hydra::TestTask.new('hydra') do |t|
62
+ # t.add_files 'test/unit/**/*_test.rb'
63
+ # t.add_files 'test/functional/**/*_test.rb'
64
+ # t.add_files 'test/integration/**/*_test.rb'
65
+ # t.verbose = false # optionally set to true for lots of debug messages
66
+ # t.autosort = false # disable automatic sorting based on runtime of tests
67
+ # end
68
+ class TestTask < Hydra::Task
69
+
70
+ # Create a new HydraTestTask
71
+ def initialize(name = :hydra)
72
+ @name = name
73
+ @files = []
74
+ @verbose = false
75
+ @autosort = true
76
+ @serial = false
77
+ @listeners = [Hydra::Listener::ProgressBar.new]
78
+
79
+ yield self if block_given?
80
+
81
+ # Ensure we override rspec's at_exit
82
+ require 'hydra/spec/autorun_override'
83
+
84
+ unless @serial
85
+ @config = find_config_file
86
+ end
87
+
88
+ @opts = {
89
+ :verbose => @verbose,
90
+ :autosort => @autosort,
91
+ :files => @files,
92
+ :listeners => @listeners
93
+ }
94
+ if @config
95
+ @opts.merge!(:config => @config)
96
+ else
97
+ @opts.merge!(:workers => [{:type => :local, :runners => 1}])
98
+ end
99
+
100
+ define
101
+ end
102
+
103
+ private
104
+ # Create the rake task defined by this HydraTestTask
105
+ def define
106
+ desc "Hydra Tests" + (@name == :hydra ? "" : " for #{@name}")
107
+ task @name do
108
+ if Object.const_defined?('RAILS_ENV') && RAILS_ENV == 'development'
109
+ $stderr.puts %{WARNING: RAILS_ENV is "development". Make sure to set it properly (ex: "RAILS_ENV=test rake hydra")}
110
+ end
111
+
112
+ Hydra::Master.new(@opts)
113
+ end
114
+ end
115
+ end
116
+
117
+ # Define a test task that uses hydra to profile your test files
118
+ #
119
+ # Hydra::ProfileTask.new('hydra:prof') do |t|
120
+ # t.add_files 'test/unit/**/*_test.rb'
121
+ # t.add_files 'test/functional/**/*_test.rb'
122
+ # t.add_files 'test/integration/**/*_test.rb'
123
+ # t.generate_html = true # defaults to false
124
+ # t.generate_text = true # defaults to true
125
+ # end
126
+ class ProfileTask < Hydra::Task
127
+ # boolean: generate html output from ruby-prof
128
+ attr_accessor :generate_html
129
+ # boolean: generate text output from ruby-prof
130
+ attr_accessor :generate_text
131
+
132
+ # Create a new Hydra ProfileTask
133
+ def initialize(name = 'hydra:profile')
134
+ @name = name
135
+ @files = []
136
+ @verbose = false
137
+ @generate_html = false
138
+ @generate_text = true
139
+
140
+ yield self if block_given?
141
+
142
+ # Ensure we override rspec's at_exit
143
+ require 'hydra/spec/autorun_override'
144
+
145
+ @config = find_config_file
146
+
147
+ @opts = {
148
+ :verbose => @verbose,
149
+ :files => @files
150
+ }
151
+ define
152
+ end
153
+
154
+ private
155
+ # Create the rake task defined by this HydraTestTask
156
+ def define
157
+ desc "Hydra Test Profile" + (@name == :hydra ? "" : " for #{@name}")
158
+ task @name do
159
+ require 'ruby-prof'
160
+ RubyProf.start
161
+
162
+ runner = Hydra::Runner.new(:io => File.new('/dev/null', 'w'))
163
+ @files.each do |file|
164
+ $stdout.write runner.run_file(file)
165
+ $stdout.flush
166
+ end
167
+
168
+ $stdout.write "\nTests complete. Generating profiling output\n"
169
+ $stdout.flush
170
+
171
+ result = RubyProf.stop
172
+
173
+ if @generate_html
174
+ printer = RubyProf::GraphHtmlPrinter.new(result)
175
+ out = File.new("ruby-prof.html", 'w')
176
+ printer.print(out, :min_self => 0.05)
177
+ out.close
178
+ $stdout.write "Profiling data written to [ruby-prof.html]\n"
179
+ end
180
+
181
+ if @generate_text
182
+ printer = RubyProf::FlatPrinter.new(result)
183
+ out = File.new("ruby-prof.txt", 'w')
184
+ printer.print(out, :min_self => 0.05)
185
+ out.close
186
+ $stdout.write "Profiling data written to [ruby-prof.txt]\n"
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ # Define a sync task that uses hydra to rsync the source tree under test to remote workers.
193
+ #
194
+ # This task is very useful to run before a remote db:reset task to make sure the db/schema.rb
195
+ # file is up to date on the remote workers.
196
+ #
197
+ # Hydra::SyncTask.new('hydra:sync') do |t|
198
+ # t.verbose = false # optionally set to true for lots of debug messages
199
+ # end
200
+ class SyncTask < Hydra::Task
201
+
202
+ # Create a new SyncTestTask
203
+ def initialize(name = :sync)
204
+ @name = name
205
+ @verbose = false
206
+
207
+ yield self if block_given?
208
+
209
+ @config = find_config_file
210
+
211
+ @opts = {
212
+ :verbose => @verbose
213
+ }
214
+ @opts.merge!(:config => @config) if @config
215
+
216
+ define
217
+ end
218
+
219
+ private
220
+ # Create the rake task defined by this HydraSyncTask
221
+ def define
222
+ desc "Hydra Tests" + (@name == :hydra ? "" : " for #{@name}")
223
+ task @name do
224
+ Hydra::Sync.sync_many(@opts)
225
+ end
226
+ end
227
+ end
228
+
229
+ # Setup a task that will be run across all remote workers
230
+ # Hydra::RemoteTask.new('db:reset')
231
+ #
232
+ # Then you can run:
233
+ # rake hydra:remote:db:reset
234
+ class RemoteTask < Hydra::Task
235
+ include Open3
236
+ # Create a new hydra remote task with the given name.
237
+ # The task will be named hydra:remote:<name>
238
+ def initialize(name)
239
+ @name = name
240
+ yield self if block_given?
241
+ @config = find_config_file
242
+ if @config
243
+ define
244
+ else
245
+ task "hydra:remote:#{@name}" do ; end
246
+ end
247
+ end
248
+
249
+ private
250
+ def define
251
+ desc "Run #{@name} remotely on all workers"
252
+ task "hydra:remote:#{@name}" do
253
+ config = YAML.load_file(@config)
254
+ environment = config.fetch('environment') { 'test' }
255
+ workers = config.fetch('workers') { [] }
256
+ workers = workers.select{|w| w['type'] == 'ssh'}
257
+
258
+ $stdout.write "==== Hydra Running #{@name} ====\n"
259
+ Thread.abort_on_exception = true
260
+ @listeners = []
261
+ @results = {}
262
+ workers.each do |worker|
263
+ @listeners << Thread.new do
264
+ begin
265
+ @results[worker] = if run_task(worker, environment)
266
+ "==== #{@name} passed on #{worker['connect']} ====\n"
267
+ else
268
+ "==== #{@name} failed on #{worker['connect']} ====\nPlease see above for more details.\n"
269
+ end
270
+ rescue
271
+ @results[worker] = "==== #{@name} failed for #{worker['connect']} ====\n#{$!.inspect}\n#{$!.backtrace.join("\n")}"
272
+ end
273
+ end
274
+ end
275
+ @listeners.each{|l| l.join}
276
+ $stdout.write "\n==== Hydra Running #{@name} COMPLETE ====\n\n"
277
+ $stdout.write @results.values.join("\n")
278
+ end
279
+ end
280
+
281
+ def run_task worker, environment
282
+ $stdout.write "==== Hydra Running #{@name} on #{worker['connect']} ====\n"
283
+ ssh_opts = worker.fetch('ssh_opts') { '' }
284
+ writer, reader, error = popen3("ssh -tt #{ssh_opts} #{worker['connect']} ")
285
+ writer.write("cd #{worker['directory']}\n")
286
+ writer.write "echo BEGIN HYDRA\n"
287
+ writer.write("RAILS_ENV=#{environment} rake #{@name}\n")
288
+ writer.write "echo END HYDRA\n"
289
+ writer.write("exit\n")
290
+ writer.close
291
+ ignoring = true
292
+ passed = true
293
+ while line = reader.gets
294
+ line.chomp!
295
+ if line =~ /^rake aborted!$/
296
+ passed = false
297
+ end
298
+ if line =~ /echo END HYDRA$/
299
+ ignoring = true
300
+ end
301
+ $stdout.write "#{worker['connect']}: #{line}\n" unless ignoring
302
+ if line == 'BEGIN HYDRA'
303
+ ignoring = false
304
+ end
305
+ end
306
+ passed
307
+ end
308
+ end
309
+
310
+ # A Hydra global task is a task that is run both locally and remotely.
311
+ #
312
+ # For example:
313
+ #
314
+ # Hydra::GlobalTask.new('db:reset')
315
+ #
316
+ # Allows you to run:
317
+ #
318
+ # rake hydra:db:reset
319
+ #
320
+ # Then, db:reset will be run locally and on all remote workers. This
321
+ # makes it easy to setup your workers and run tasks all in a row.
322
+ #
323
+ # For example:
324
+ #
325
+ # rake hydra:db:reset hydra:factories hydra:tests
326
+ #
327
+ # Assuming you setup hydra:db:reset and hydra:db:factories as global
328
+ # tasks and hydra:tests as a Hydra::TestTask for all your tests
329
+ class GlobalTask < Hydra::Task
330
+ def initialize(name)
331
+ @name = name
332
+ define
333
+ end
334
+
335
+ private
336
+ def define
337
+ Hydra::RemoteTask.new(@name)
338
+ desc "Run #{@name.to_s} Locally and Remotely across all Workers"
339
+ task "hydra:#{@name.to_s}" => [@name.to_s, "hydra:remote:#{@name.to_s}"]
340
+ end
341
+ end
342
+ end