justinf-hydra 0.23.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) 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.rb +16 -0
  12. data/lib/hydra/cucumber/formatter.rb +29 -0
  13. data/lib/hydra/hash.rb +16 -0
  14. data/lib/hydra/js/lint.js +5150 -0
  15. data/lib/hydra/listener/abstract.rb +39 -0
  16. data/lib/hydra/listener/minimal_output.rb +24 -0
  17. data/lib/hydra/listener/notifier.rb +17 -0
  18. data/lib/hydra/listener/progress_bar.rb +48 -0
  19. data/lib/hydra/listener/report_generator.rb +30 -0
  20. data/lib/hydra/master.rb +248 -0
  21. data/lib/hydra/message.rb +47 -0
  22. data/lib/hydra/message/master_messages.rb +19 -0
  23. data/lib/hydra/message/runner_messages.rb +46 -0
  24. data/lib/hydra/message/worker_messages.rb +52 -0
  25. data/lib/hydra/messaging_io.rb +49 -0
  26. data/lib/hydra/pipe.rb +61 -0
  27. data/lib/hydra/proxy_config.rb +27 -0
  28. data/lib/hydra/runner.rb +306 -0
  29. data/lib/hydra/runner_listener/abstract.rb +23 -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 +366 -0
  37. data/lib/hydra/tmpdir.rb +11 -0
  38. data/lib/hydra/trace.rb +24 -0
  39. data/lib/hydra/worker.rb +168 -0
  40. data/test/fixtures/assert_true.rb +7 -0
  41. data/test/fixtures/bad_proxy_config.yml +4 -0
  42. data/test/fixtures/config.yml +4 -0
  43. data/test/fixtures/conflicting.rb +10 -0
  44. data/test/fixtures/features/step_definitions.rb +21 -0
  45. data/test/fixtures/features/write_alternate_file.feature +7 -0
  46. data/test/fixtures/features/write_file.feature +7 -0
  47. data/test/fixtures/hello_world.rb +3 -0
  48. data/test/fixtures/hydra_worker_init.rb +2 -0
  49. data/test/fixtures/js_file.js +4 -0
  50. data/test/fixtures/json_data.json +4 -0
  51. data/test/fixtures/many_outputs_to_console.rb +9 -0
  52. data/test/fixtures/master_listeners.rb +10 -0
  53. data/test/fixtures/proxy_config.yml +4 -0
  54. data/test/fixtures/proxy_config_http.yml +4 -0
  55. data/test/fixtures/runner_listeners.rb +23 -0
  56. data/test/fixtures/slow.rb +9 -0
  57. data/test/fixtures/sync_test.rb +8 -0
  58. data/test/fixtures/task_test_config.yml +6 -0
  59. data/test/fixtures/write_file.rb +10 -0
  60. data/test/fixtures/write_file_alternate_spec.rb +10 -0
  61. data/test/fixtures/write_file_spec.rb +9 -0
  62. data/test/fixtures/write_file_with_pending_spec.rb +11 -0
  63. data/test/master_test.rb +383 -0
  64. data/test/message_test.rb +31 -0
  65. data/test/pipe_test.rb +38 -0
  66. data/test/proxy_config_test.rb +31 -0
  67. data/test/runner_test.rb +196 -0
  68. data/test/ssh_test.rb +25 -0
  69. data/test/sync_test.rb +113 -0
  70. data/test/task_test.rb +21 -0
  71. data/test/test_helper.rb +107 -0
  72. data/test/worker_test.rb +60 -0
  73. metadata +206 -0
@@ -0,0 +1,39 @@
1
+ module Hydra #:nodoc:
2
+ module Listener #: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 when testing has started
15
+ def testing_begin(files)
16
+ end
17
+
18
+ # Fired when testing finishes, after the workers shutdown
19
+ def testing_end
20
+ end
21
+
22
+ # Fired after runner processes have been started
23
+ def worker_begin(worker)
24
+ end
25
+
26
+ # Fired before shutting down the worker
27
+ def worker_end(worker)
28
+ end
29
+
30
+ # Fired when a file is started
31
+ def file_begin(file)
32
+ end
33
+
34
+ # Fired when a file is finished
35
+ def file_end(file, output)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,24 @@
1
+ module Hydra #:nodoc:
2
+ module Listener #:nodoc:
3
+ # Minimal output listener. Outputs all the files at the start
4
+ # of testing and outputs a ./F/E per file. As well as
5
+ # full error output, if any.
6
+ class MinimalOutput < Hydra::Listener::Abstract
7
+ # output a starting message
8
+ def testing_begin(files)
9
+ @output.write "Hydra Testing:\n#{files.inspect}\n"
10
+ end
11
+
12
+ # output a finished message
13
+ def testing_end
14
+ @output.write "\nHydra Completed\n"
15
+ end
16
+
17
+ # For each file, just output a . for a successful file, or the
18
+ # Failure/Error output from the tests
19
+ def file_end(file, output)
20
+ @output.write output
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ module Hydra #:nodoc:
2
+ module Listener #:nodoc:
3
+ # Sends a command to Notifier when the testing has finished
4
+ # http://manpages.ubuntu.com/manpages/gutsy/man1/notify-send.1.html
5
+ class Notifier < Hydra::Listener::Abstract
6
+ # output a finished notification
7
+ def testing_end
8
+ icon_path = File.join(
9
+ File.dirname(__FILE__), '..', '..', '..',
10
+ 'hydra-icon-64x64.png'
11
+ )
12
+ `notify-send -i #{icon_path} "Hydra" "Testing Completed"`
13
+ end
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,48 @@
1
+ module Hydra #:nodoc:
2
+ module Listener #:nodoc:
3
+ # Output a progress bar as files are completed
4
+ class ProgressBar < Hydra::Listener::Abstract
5
+ # Store the total number of files
6
+ def testing_begin(files)
7
+ @total_files = files.size
8
+ @files_completed = 0
9
+ @test_output = ""
10
+ @errors = false
11
+ render_progress_bar
12
+ end
13
+
14
+ # Increment completed files count and update bar
15
+ def file_end(file, output)
16
+ unless output == '.'
17
+ @output.write "\r#{' '*60}\r#{output}\n"
18
+ @errors = true
19
+ end
20
+ @files_completed += 1
21
+ render_progress_bar
22
+ end
23
+
24
+ # Break the line
25
+ def testing_end
26
+ render_progress_bar
27
+ @output.write "\n"
28
+ end
29
+
30
+ private
31
+
32
+ def render_progress_bar
33
+ width = 30
34
+ complete = ((@files_completed.to_f / @total_files.to_f) * width).to_i
35
+ @output.write "\r" # move to beginning
36
+ @output.write 'Hydra Testing ['
37
+ @output.write @errors ? "\033[0;31m" : "\033[0;32m"
38
+ complete.times{@output.write '#'}
39
+ @output.write '>'
40
+ (width-complete).times{@output.write ' '}
41
+ @output.write "\033[0m"
42
+ @output.write "] #{@files_completed}/#{@total_files}"
43
+ @output.flush
44
+ end
45
+ end
46
+ end
47
+ end
48
+
@@ -0,0 +1,30 @@
1
+ module Hydra #:nodoc:
2
+ module Listener #:nodoc:
3
+ # Output a textual report at the end of testing
4
+ class ReportGenerator < Hydra::Listener::Abstract
5
+ # Initialize a new report
6
+ def testing_begin(files)
7
+ @report = { }
8
+ end
9
+
10
+ # Log the start time of a file
11
+ def file_begin(file)
12
+ @report[file] ||= { }
13
+ @report[file]['start'] = Time.now.to_f
14
+ end
15
+
16
+ # Log the end time of a file and compute the file's testing
17
+ # duration
18
+ def file_end(file, output)
19
+ @report[file]['end'] = Time.now.to_f
20
+ @report[file]['duration'] = @report[file]['end'] - @report[file]['start']
21
+ end
22
+
23
+ # output the report
24
+ def testing_end
25
+ YAML.dump(@report, @output)
26
+ @output.close
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,248 @@
1
+ require 'hydra/hash'
2
+ require 'open3'
3
+ require 'hydra/tmpdir'
4
+ require 'hydra/proxy_config'
5
+ require 'erb'
6
+ require 'yaml'
7
+
8
+ module Hydra #:nodoc:
9
+ # Hydra class responsible for delegate work down to workers.
10
+ #
11
+ # The Master is run once for any given testing session.
12
+ class YmlLoadError < StandardError; end
13
+
14
+ class Master
15
+ include Hydra::Messages::Master
16
+ include Open3
17
+ traceable('MASTER')
18
+ attr_reader :failed_files
19
+
20
+ # Create a new Master
21
+ #
22
+ # Options:
23
+ # * :files
24
+ # * An array of test files to be run. These should be relative paths from
25
+ # the root of the project, since they may be run on different machines
26
+ # which may have different paths.
27
+ # * :workers
28
+ # * An array of hashes. Each hash should be the configuration options
29
+ # for a worker.
30
+ # * :listeners
31
+ # * An array of Hydra::Listener objects. See Hydra::Listener::MinimalOutput for an
32
+ # example listener
33
+ # * :verbose
34
+ # * Set to true to see lots of Hydra output (for debugging)
35
+ # * :autosort
36
+ # * Set to false to disable automatic sorting by historical run-time per file
37
+ def initialize(opts = { })
38
+ opts.stringify_keys!
39
+ config_file = opts.delete('config') { nil }
40
+ if config_file
41
+
42
+ begin
43
+ config_erb = ERB.new(IO.read(config_file)).result(binding)
44
+ rescue Exception => e
45
+ raise(YmlLoadError,"config file was found, but could not be parsed with ERB.\n#{$!.inspect}")
46
+ end
47
+
48
+ begin
49
+ config_yml = ProxyConfig.load(config_erb)
50
+ rescue StandardError => e
51
+ raise(YmlLoadError,"config file was found, but could not be parsed.\n#{$!.inspect}")
52
+ end
53
+
54
+ opts.merge!(config_yml.stringify_keys!)
55
+ end
56
+ @files = Array(opts.fetch('files') { nil })
57
+ raise "No files, nothing to do" if @files.empty?
58
+ @incomplete_files = @files.dup
59
+ @failed_files = []
60
+ @workers = []
61
+ @listeners = []
62
+ @event_listeners = Array(opts.fetch('listeners') { nil } )
63
+ @event_listeners.select{|l| l.is_a? String}.each do |l|
64
+ @event_listeners.delete_at(@event_listeners.index(l))
65
+ listener = eval(l)
66
+ @event_listeners << listener if listener.is_a?(Hydra::Listener::Abstract)
67
+ end
68
+
69
+ @string_runner_event_listeners = Array( opts.fetch( 'runner_listeners' ) { nil } )
70
+
71
+ @runner_log_file = opts.fetch('runner_log_file') { nil }
72
+ @verbose = opts.fetch('verbose') { false }
73
+ @autosort = opts.fetch('autosort') { true }
74
+ @sync = opts.fetch('sync') { nil }
75
+ @environment = opts.fetch('environment') { 'test' }
76
+
77
+ if @autosort
78
+ sort_files_from_report
79
+ @event_listeners << Hydra::Listener::ReportGenerator.new(File.new(heuristic_file, 'w'))
80
+ end
81
+
82
+ # default is one worker that is configured to use a pipe with one runner
83
+ worker_cfg = opts.fetch('workers') { [ { 'type' => 'local', 'runners' => 1} ] }
84
+
85
+ trace "Initialized"
86
+ trace " Files: (#{@files.inspect})"
87
+ trace " Workers: (#{worker_cfg.inspect})"
88
+ trace " Verbose: (#{@verbose.inspect})"
89
+
90
+ @event_listeners.each{|l| l.testing_begin(@files) }
91
+
92
+ boot_workers worker_cfg
93
+ process_messages
94
+ end
95
+
96
+ # Message handling
97
+ def worker_begin(worker)
98
+ @event_listeners.each {|l| l.worker_begin(worker) }
99
+ end
100
+
101
+ # Send a file down to a worker.
102
+ def send_file(worker)
103
+ f = @files.shift
104
+ if f
105
+ trace "Sending #{f.inspect}"
106
+ @event_listeners.each{|l| l.file_begin(f) }
107
+ worker[:io].write(RunFile.new(:file => f))
108
+ else
109
+ trace "No more files to send"
110
+ end
111
+ end
112
+
113
+ # Process the results coming back from the worker.
114
+ def process_results(worker, message)
115
+ if message.output =~ /ActiveRecord::StatementInvalid(.*)[Dd]eadlock/ or
116
+ message.output =~ /PGError: ERROR(.*)[Dd]eadlock/ or
117
+ message.output =~ /Mysql::Error: SAVEPOINT(.*)does not exist: ROLLBACK/ or
118
+ message.output =~ /Mysql::Error: Deadlock found/
119
+ trace "Deadlock detected running [#{message.file}]. Will retry at the end"
120
+ @files.push(message.file)
121
+ send_file(worker)
122
+ else
123
+ @incomplete_files.delete_at(@incomplete_files.index(message.file))
124
+ trace "#{@incomplete_files.size} Files Remaining"
125
+ @event_listeners.each{|l| l.file_end(message.file, message.output) }
126
+ unless message.output == '.'
127
+ @failed_files << message.file
128
+ end
129
+ if @incomplete_files.empty?
130
+ @workers.each do |worker|
131
+ @event_listeners.each{|l| l.worker_end(worker) }
132
+ end
133
+
134
+ shutdown_all_workers
135
+ else
136
+ send_file(worker)
137
+ end
138
+ end
139
+ end
140
+
141
+ # A text report of the time it took to run each file
142
+ attr_reader :report_text
143
+
144
+ private
145
+
146
+ def boot_workers(workers)
147
+ trace "Booting #{workers.size} workers"
148
+ workers.each do |worker|
149
+ worker.stringify_keys!
150
+ trace "worker opts #{worker.inspect}"
151
+ type = worker.fetch('type') { 'local' }
152
+ if type.to_s == 'local'
153
+ boot_local_worker(worker)
154
+ elsif type.to_s == 'ssh'
155
+ @workers << worker # will boot later, during the listening phase
156
+ else
157
+ raise "Worker type not recognized: (#{type.to_s})"
158
+ end
159
+ end
160
+ end
161
+
162
+ def boot_local_worker(worker)
163
+ runners = worker.fetch('runners') { raise "You must specify the number of runners" }
164
+ trace "Booting local worker"
165
+ pipe = Hydra::Pipe.new
166
+ child = SafeFork.fork do
167
+ pipe.identify_as_child
168
+ Hydra::Worker.new(:io => pipe, :runners => runners, :verbose => @verbose, :runner_listeners => @string_runner_event_listeners, :runner_log_file => @runner_log_file )
169
+ end
170
+
171
+ pipe.identify_as_parent
172
+ @workers << { :pid => child, :io => pipe, :idle => false, :type => :local }
173
+ end
174
+
175
+ def boot_ssh_worker(worker)
176
+ sync = Sync.new(worker, @sync, @verbose)
177
+
178
+ runners = worker.fetch('runners') { raise "You must specify the number of runners" }
179
+ command = worker.fetch('command') {
180
+ "RAILS_ENV=#{@environment} ruby -e \"require 'rubygems'; require 'hydra'; Hydra::Worker.new(:io => Hydra::Stdio.new, :runners => #{runners}, :verbose => #{@verbose}, :runner_listeners => \'#{@string_runner_event_listeners}\', :runner_log_file => \'#{@runner_log_file}\' );\""
181
+ }
182
+
183
+ trace "Booting SSH worker"
184
+ ssh = Hydra::SSH.new("#{sync.ssh_opts} #{sync.connect}", sync.remote_dir, command)
185
+ return { :io => ssh, :idle => false, :type => :ssh, :connect => sync.connect }
186
+ end
187
+
188
+ def shutdown_all_workers
189
+ trace "Shutting down all workers"
190
+ @workers.each do |worker|
191
+ worker[:io].write(Shutdown.new) if worker[:io]
192
+ worker[:io].close if worker[:io]
193
+ end
194
+ @listeners.each{|t| t.exit}
195
+ end
196
+
197
+ def process_messages
198
+ Thread.abort_on_exception = true
199
+
200
+ trace "Processing Messages"
201
+ trace "Workers: #{@workers.inspect}"
202
+ @workers.each do |worker|
203
+ @listeners << Thread.new do
204
+ trace "Listening to #{worker.inspect}"
205
+ if worker.fetch('type') { 'local' }.to_s == 'ssh'
206
+ worker = boot_ssh_worker(worker)
207
+ @workers << worker
208
+ end
209
+ while true
210
+ begin
211
+ message = worker[:io].gets
212
+ trace "got message: #{message}"
213
+ # if it exists and its for me.
214
+ # SSH gives us back echoes, so we need to ignore our own messages
215
+ if message and !message.class.to_s.index("Worker").nil?
216
+ message.handle(self, worker)
217
+ end
218
+ rescue IOError
219
+ trace "lost Worker [#{worker.inspect}]"
220
+ Thread.exit
221
+ end
222
+ end
223
+ end
224
+ end
225
+
226
+ @listeners.each{|l| l.join}
227
+ @event_listeners.each{|l| l.testing_end}
228
+ end
229
+
230
+ def sort_files_from_report
231
+ if File.exists? heuristic_file
232
+ report = YAML.load_file(heuristic_file)
233
+ return unless report
234
+ sorted_files = report.sort{ |a,b|
235
+ b[1]['duration'] <=> a[1]['duration']
236
+ }.collect{|tuple| tuple[0]}
237
+
238
+ sorted_files.each do |f|
239
+ @files.push(@files.delete_at(@files.index(f))) if @files.index(f)
240
+ end
241
+ end
242
+ end
243
+
244
+ def heuristic_file
245
+ @heuristic_file ||= File.join(Dir.consistent_tmpdir, 'hydra_heuristics.yml')
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,47 @@
1
+ module Hydra #:nodoc:
2
+ # Base message object. Used to pass messages with parameters around
3
+ # via IO objects.
4
+ # class MyMessage < Hydra::Message
5
+ # attr_accessor :my_var
6
+ # def serialize
7
+ # super(:my_var => @my_var)
8
+ # end
9
+ # end
10
+ # m = MyMessage.new(:my_var => 'my value')
11
+ # m.my_var
12
+ # => "my value"
13
+ # m.serialize
14
+ # => "{:class=>TestMessage::MyMessage, :my_var=>\"my value\"}"
15
+ # Hydra::Message.build(eval(@m.serialize)).my_var
16
+ # => "my value"
17
+ class Message
18
+ # Create a new message. Opts is a hash where the keys
19
+ # are attributes of the message and the values are
20
+ # set to the attribute.
21
+ def initialize(opts = {})
22
+ opts.delete :class
23
+ opts.each do |variable,value|
24
+ self.send("#{variable}=",value)
25
+ end
26
+ end
27
+
28
+ # Build a message from a hash. The hash must contain
29
+ # the :class symbol, which is the class of the message
30
+ # that it will build to.
31
+ def self.build(hash)
32
+ hash.delete(:class).new(hash)
33
+ end
34
+
35
+ # Serialize the message for output on an IO channel.
36
+ # This is really just a string representation of a hash
37
+ # with no newlines. It adds in the class automatically
38
+ def serialize(opts = {})
39
+ opts.merge({:class => self.class}).inspect
40
+ end
41
+ end
42
+ end
43
+
44
+ require 'hydra/message/runner_messages'
45
+ require 'hydra/message/worker_messages'
46
+ require 'hydra/message/master_messages'
47
+