arturop-hydra 0.23.4

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