causes-hydra 0.21.0

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