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