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,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
+