sskirby-hydra 0.16.9

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 (57) 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 +55 -0
  6. data/TODO +18 -0
  7. data/VERSION +1 -0
  8. data/caliper.yml +6 -0
  9. data/hydra-icon-64x64.png +0 -0
  10. data/hydra.gemspec +122 -0
  11. data/hydra_gray.png +0 -0
  12. data/lib/hydra/cucumber/formatter.rb +30 -0
  13. data/lib/hydra/hash.rb +16 -0
  14. data/lib/hydra/listener/abstract.rb +30 -0
  15. data/lib/hydra/listener/minimal_output.rb +24 -0
  16. data/lib/hydra/listener/notifier.rb +17 -0
  17. data/lib/hydra/listener/progress_bar.rb +48 -0
  18. data/lib/hydra/listener/report_generator.rb +30 -0
  19. data/lib/hydra/master.rb +224 -0
  20. data/lib/hydra/message/master_messages.rb +19 -0
  21. data/lib/hydra/message/runner_messages.rb +46 -0
  22. data/lib/hydra/message/worker_messages.rb +46 -0
  23. data/lib/hydra/message.rb +47 -0
  24. data/lib/hydra/messaging_io.rb +48 -0
  25. data/lib/hydra/pipe.rb +61 -0
  26. data/lib/hydra/runner.rb +214 -0
  27. data/lib/hydra/safe_fork.rb +31 -0
  28. data/lib/hydra/spec/autorun_override.rb +12 -0
  29. data/lib/hydra/spec/hydra_formatter.rb +17 -0
  30. data/lib/hydra/ssh.rb +40 -0
  31. data/lib/hydra/stdio.rb +16 -0
  32. data/lib/hydra/sync.rb +99 -0
  33. data/lib/hydra/tasks.rb +256 -0
  34. data/lib/hydra/trace.rb +24 -0
  35. data/lib/hydra/worker.rb +146 -0
  36. data/lib/hydra.rb +16 -0
  37. data/test/fixtures/assert_true.rb +7 -0
  38. data/test/fixtures/config.yml +4 -0
  39. data/test/fixtures/features/step_definitions.rb +21 -0
  40. data/test/fixtures/features/write_alternate_file.feature +7 -0
  41. data/test/fixtures/features/write_file.feature +7 -0
  42. data/test/fixtures/hello_world.rb +3 -0
  43. data/test/fixtures/slow.rb +9 -0
  44. data/test/fixtures/sync_test.rb +8 -0
  45. data/test/fixtures/write_file.rb +10 -0
  46. data/test/fixtures/write_file_alternate_spec.rb +10 -0
  47. data/test/fixtures/write_file_spec.rb +9 -0
  48. data/test/fixtures/write_file_with_pending_spec.rb +11 -0
  49. data/test/master_test.rb +152 -0
  50. data/test/message_test.rb +31 -0
  51. data/test/pipe_test.rb +38 -0
  52. data/test/runner_test.rb +144 -0
  53. data/test/ssh_test.rb +14 -0
  54. data/test/sync_test.rb +113 -0
  55. data/test/test_helper.rb +60 -0
  56. data/test/worker_test.rb +58 -0
  57. metadata +179 -0
@@ -0,0 +1,224 @@
1
+ require 'hydra/hash'
2
+ require 'open3'
3
+ require 'tmpdir'
4
+ require 'yaml'
5
+ module Hydra #:nodoc:
6
+ # Hydra class responsible for delegate work down to workers.
7
+ #
8
+ # The Master is run once for any given testing session.
9
+ class Master
10
+ include Hydra::Messages::Master
11
+ include Open3
12
+ traceable('MASTER')
13
+
14
+ # Create a new Master
15
+ #
16
+ # Options:
17
+ # * :files
18
+ # * An array of test files to be run. These should be relative paths from
19
+ # the root of the project, since they may be run on different machines
20
+ # which may have different paths.
21
+ # * :workers
22
+ # * An array of hashes. Each hash should be the configuration options
23
+ # for a worker.
24
+ # * :listeners
25
+ # * An array of Hydra::Listener objects. See Hydra::Listener::MinimalOutput for an
26
+ # example listener
27
+ # * :verbose
28
+ # * Set to true to see lots of Hydra output (for debugging)
29
+ # * :autosort
30
+ # * Set to false to disable automatic sorting by historical run-time per file
31
+ def initialize(opts = { })
32
+ #at_exit do
33
+ trap("SIGINT") do
34
+ begin
35
+ puts "Testing halted by user. Untested files:"
36
+ puts @incomplete_files.join("\n")
37
+ rescue
38
+ trace $!.inspect
39
+ trace $!.backtrace.join("\n")
40
+ end
41
+ exit
42
+ end
43
+ opts.stringify_keys!
44
+ config_file = opts.delete('config') { nil }
45
+ if config_file
46
+ opts.merge!(YAML.load_file(config_file).stringify_keys!)
47
+ end
48
+ @files = Array(opts.fetch('files') { nil })
49
+ raise "No files, nothing to do" if @files.empty?
50
+ @incomplete_files = @files.dup
51
+ @workers = []
52
+ @listeners = []
53
+ @event_listeners = Array(opts.fetch('listeners') { nil } )
54
+ @event_listeners.select{|l| l.is_a? String}.each do |l|
55
+ @event_listeners.delete_at(@event_listeners.index(l))
56
+ listener = eval(l)
57
+ @event_listeners << listener if listener.is_a?(Hydra::Listener::Abstract)
58
+ end
59
+ @verbose = opts.fetch('verbose') { false }
60
+ @autosort = opts.fetch('autosort') { true }
61
+ @sync = opts.fetch('sync') { nil }
62
+ @environment = opts.fetch('environment') { 'test' }
63
+
64
+ if @autosort
65
+ sort_files_from_report
66
+ @event_listeners << Hydra::Listener::ReportGenerator.new(File.new(heuristic_file, 'w'))
67
+ end
68
+
69
+ # default is one worker that is configured to use a pipe with one runner
70
+ worker_cfg = opts.fetch('workers') { [ { 'type' => 'local', 'runners' => 1} ] }
71
+
72
+ trace "Initialized"
73
+ trace " Files: (#{@files.inspect})"
74
+ trace " Workers: (#{worker_cfg.inspect})"
75
+ trace " Verbose: (#{@verbose.inspect})"
76
+
77
+ @event_listeners.each{|l| l.testing_begin(@files) }
78
+
79
+ boot_workers worker_cfg
80
+ process_messages
81
+ end
82
+
83
+ # Message handling
84
+
85
+ # Send a file down to a worker.
86
+ def send_file(worker)
87
+ f = @files.shift
88
+ if f
89
+ trace "Sending #{f.inspect}"
90
+ @event_listeners.each{|l| l.file_begin(f) }
91
+ worker[:io].write(RunFile.new(:file => f))
92
+ else
93
+ trace "No more files to send"
94
+ end
95
+ end
96
+
97
+ # Process the results coming back from the worker.
98
+ def process_results(worker, message)
99
+ if message.output =~ /ActiveRecord::StatementInvalid(.*)[Dd]eadlock/ or
100
+ message.output =~ /PGError: ERROR(.*)[Dd]eadlock/ or
101
+ message.output =~ /Mysql::Error: SAVEPOINT(.*)does not exist: ROLLBACK/
102
+ trace "Deadlock detected running [#{message.file}]. Will retry at the end"
103
+ @files.push(message.file)
104
+ else
105
+ @incomplete_files.delete_at(@incomplete_files.index(message.file))
106
+ trace "#{@incomplete_files.size} Files Remaining"
107
+ @event_listeners.each{|l| l.file_end(message.file, message.output) }
108
+ if @incomplete_files.empty?
109
+ shutdown_all_workers
110
+ else
111
+ send_file(worker)
112
+ end
113
+ end
114
+ end
115
+
116
+ # A text report of the time it took to run each file
117
+ attr_reader :report_text
118
+
119
+ private
120
+
121
+ def boot_workers(workers)
122
+ trace "Booting #{workers.size} workers"
123
+ workers.each do |worker|
124
+ worker.stringify_keys!
125
+ trace "worker opts #{worker.inspect}"
126
+ type = worker.fetch('type') { 'local' }
127
+ if type.to_s == 'local'
128
+ boot_local_worker(worker)
129
+ elsif type.to_s == 'ssh'
130
+ @workers << worker # will boot later, during the listening phase
131
+ else
132
+ raise "Worker type not recognized: (#{type.to_s})"
133
+ end
134
+ end
135
+ end
136
+
137
+ def boot_local_worker(worker)
138
+ runners = worker.fetch('runners') { raise "You must specify the number of runners" }
139
+ trace "Booting local worker"
140
+ pipe = Hydra::Pipe.new
141
+ child = SafeFork.fork do
142
+ pipe.identify_as_child
143
+ Hydra::Worker.new(:io => pipe, :runners => runners, :verbose => @verbose)
144
+ end
145
+ pipe.identify_as_parent
146
+ @workers << { :pid => child, :io => pipe, :idle => false, :type => :local }
147
+ end
148
+
149
+ def boot_ssh_worker(worker)
150
+ sync = Sync.new(worker, @sync, @verbose)
151
+
152
+ runners = worker.fetch('runners') { raise "You must specify the number of runners" }
153
+ command = worker.fetch('command') {
154
+ "RAILS_ENV=#{@environment} ruby -e \"require 'rubygems'; require 'hydra'; Hydra::Worker.new(:io => Hydra::Stdio.new, :runners => #{runners}, :verbose => #{@verbose});\""
155
+ }
156
+
157
+ trace "Booting SSH worker"
158
+ ssh = Hydra::SSH.new("#{sync.ssh_opts} #{sync.connect}", sync.remote_dir, command)
159
+ return { :io => ssh, :idle => false, :type => :ssh, :connect => sync.connect }
160
+ end
161
+
162
+ def shutdown_all_workers
163
+ trace "Shutting down all workers"
164
+ @workers.each do |worker|
165
+ worker[:io].write(Shutdown.new) if worker[:io]
166
+ worker[:io].close if worker[:io]
167
+ end
168
+ @listeners.each{|t| t.exit}
169
+ end
170
+
171
+ def process_messages
172
+ Thread.abort_on_exception = true
173
+
174
+ trace "Processing Messages"
175
+ trace "Workers: #{@workers.inspect}"
176
+ @workers.each do |worker|
177
+ @listeners << Thread.new do
178
+ trace "Listening to #{worker.inspect}"
179
+ if worker.fetch('type') { 'local' }.to_s == 'ssh'
180
+ worker = boot_ssh_worker(worker)
181
+ @workers << worker
182
+ end
183
+ while true
184
+ begin
185
+ message = worker[:io].gets
186
+ trace "got message: '#{message}'"
187
+ # if it exists and its for me.
188
+ # SSH gives us back echoes, so we need to ignore our own messages
189
+ if message and !message.class.to_s.index("Worker").nil?
190
+ message.handle(self, worker)
191
+ end
192
+ rescue IOError
193
+ trace "lost Worker [#{worker.inspect}]"
194
+ trace $!.inspect
195
+ trace $!.backtrace.join("\n")
196
+ Thread.exit
197
+ end
198
+ end
199
+ end
200
+ end
201
+
202
+ @listeners.each{|l| l.join}
203
+ @event_listeners.each{|l| l.testing_end}
204
+ end
205
+
206
+ def sort_files_from_report
207
+ if File.exists? heuristic_file
208
+ report = YAML.load_file(heuristic_file)
209
+ return unless report
210
+ sorted_files = report.sort{ |a,b|
211
+ b[1]['duration'] <=> a[1]['duration']
212
+ }.collect{|tuple| tuple[0]}
213
+
214
+ sorted_files.each do |f|
215
+ @files.push(@files.delete_at(@files.index(f))) if @files.index(f)
216
+ end
217
+ end
218
+ end
219
+
220
+ def heuristic_file
221
+ @heuristic_file ||= File.join(Dir.tmpdir, 'hydra_heuristics.yml')
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,19 @@
1
+ module Hydra #:nodoc:
2
+ module Messages #:nodoc:
3
+ module Master #:nodoc:
4
+ # Message telling a worker to delegate a file to a runner
5
+ class RunFile < Hydra::Messages::Worker::RunFile
6
+ def handle(worker) #:nodoc:
7
+ worker.delegate_file(self)
8
+ end
9
+ end
10
+
11
+ # Message telling the worker to shut down.
12
+ class Shutdown < Hydra::Messages::Worker::Shutdown
13
+ def handle(worker) #:nodoc:
14
+ worker.shutdown
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,46 @@
1
+ module Hydra #:nodoc:
2
+ module Messages #:nodoc:
3
+ module Runner #:nodoc:
4
+ # Message indicating that a Runner needs a file to run
5
+ class RequestFile < Hydra::Message
6
+ def handle(worker, runner) #:nodoc:
7
+ worker.request_file(self, runner)
8
+ end
9
+ end
10
+
11
+ # Message for the Runner to respond with its results
12
+ class Results < Hydra::Message
13
+ # The output from running the test
14
+ attr_accessor :output
15
+ # The file that was run
16
+ attr_accessor :file
17
+ def serialize #:nodoc:
18
+ super(:output => @output, :file => @file)
19
+ end
20
+ def handle(worker, runner) #:nodoc:
21
+ worker.relay_results(self, runner)
22
+ end
23
+ end
24
+
25
+ # Message a runner sends to a worker to verify the connection
26
+ class Ping < Hydra::Message
27
+ def handle(worker, runner) #:nodoc:
28
+ # We don't do anything to handle a ping. It's just to test
29
+ # the connectivity of the IO
30
+ end
31
+ end
32
+
33
+ # The runner forks to run rspec messages
34
+ # so that specs don't get rerun. It uses
35
+ # this message to report the results. See
36
+ # Runner::run_rspec_file.
37
+ class RSpecResult < Hydra::Message
38
+ # the output of the spec
39
+ attr_accessor :output
40
+ def serialize #:nodoc:
41
+ super(:output => @output)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ module Hydra #:nodoc:
2
+ module Messages #:nodoc:
3
+ module Worker #:nodoc:
4
+ # Message indicating that a worker needs a file to delegate to a runner
5
+ class RequestFile < Hydra::Message
6
+ def handle(master, worker) #:nodoc:
7
+ master.send_file(worker)
8
+ end
9
+ end
10
+
11
+ # Message telling the Runner to run a file
12
+ class RunFile < Hydra::Message
13
+ # The file that should be run
14
+ attr_accessor :file
15
+ def serialize #:nodoc:
16
+ super(:file => @file)
17
+ end
18
+ def handle(runner) #:nodoc:
19
+ runner.run_file(@file)
20
+ end
21
+ end
22
+
23
+ # Message to tell the Runner to shut down
24
+ class Shutdown < Hydra::Message
25
+ def handle(runner) #:nodoc:
26
+ runner.stop
27
+ end
28
+ end
29
+
30
+ # Message relaying the results of a worker up to the master
31
+ class Results < Hydra::Messages::Runner::Results
32
+ def handle(master, worker) #:nodoc:
33
+ master.process_results(worker, self)
34
+ end
35
+ end
36
+
37
+ # Message a worker sends to a master to verify the connection
38
+ class Ping < Hydra::Message
39
+ def handle(master, worker) #:nodoc:
40
+ # We don't do anything to handle a ping. It's just to test
41
+ # the connectivity of the IO
42
+ end
43
+ end
44
+ end
45
+ end
46
+ 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
+
@@ -0,0 +1,48 @@
1
+ module Hydra #:nodoc:
2
+ # Module that implemets methods that auto-serialize and deserialize messaging
3
+ # objects.
4
+ module MessagingIO
5
+ # Read a Message from the input IO object. Automatically build
6
+ # a message from the response and return it.
7
+ #
8
+ # IO.gets
9
+ # => Hydra::Message # or subclass
10
+ def gets
11
+ raise IOError unless @reader
12
+ message = @reader.gets
13
+ return nil unless message
14
+ return Message.build(eval(message.chomp))
15
+ rescue SyntaxError, NameError
16
+ # uncomment to help catch remote errors by seeing all traffic
17
+ $stderr.write "Not a message: [#{message.inspect}]\n"
18
+ return gets
19
+ end
20
+
21
+ # Write a Message to the output IO object. It will automatically
22
+ # serialize a Message object.
23
+ # IO.write Hydra::Message.new
24
+ def write(message)
25
+ raise IOError unless @writer
26
+ raise UnprocessableMessage unless message.is_a?(Hydra::Message)
27
+ @writer.write(message.serialize+"\n")
28
+ rescue Errno::EPIPE
29
+ $stderr.write $!.inspect
30
+ $stderr.write $!.backtrace.join("\n")
31
+ raise IOError
32
+ end
33
+
34
+ # Closes the IO object.
35
+ def close
36
+ @reader.close if @reader
37
+ @writer.close if @writer
38
+ end
39
+
40
+ # IO will return this error if it cannot process a message.
41
+ # For example, if you tried to write a string, it would fail,
42
+ # because the string is not a message.
43
+ class UnprocessableMessage < RuntimeError
44
+ # Custom error message
45
+ attr_accessor :message
46
+ end
47
+ end
48
+ end
data/lib/hydra/pipe.rb ADDED
@@ -0,0 +1,61 @@
1
+ require 'hydra/messaging_io'
2
+ module Hydra #:nodoc:
3
+ # Read and write between two processes via pipes. For example:
4
+ # @pipe = Hydra::Pipe.new
5
+ # @child = Process.fork do
6
+ # @pipe.identify_as_child
7
+ # puts "A message from my parent:\n#{@pipe.gets.text}"
8
+ # @pipe.close
9
+ # end
10
+ # @pipe.identify_as_parent
11
+ # @pipe.write Hydra::Messages::TestMessage.new(:text => "Hello!")
12
+ # @pipe.close
13
+ #
14
+ # Note that the TestMessage class is only available in tests, and
15
+ # not in Hydra by default.
16
+ #
17
+ #
18
+ # When the process forks, the pipe is copied. When a pipe is
19
+ # identified as a parent or child, it is choosing which ends
20
+ # of the pipe to use.
21
+ #
22
+ # A pipe is actually two pipes:
23
+ #
24
+ # Parent == Pipe 1 ==> Child
25
+ # Parent <== Pipe 2 == Child
26
+ #
27
+ # It's like if you had two cardboard tubes and you were using
28
+ # them to drop balls with messages in them between processes.
29
+ # One tube is for sending from parent to child, and the other
30
+ # tube is for sending from child to parent.
31
+ class Pipe
32
+ include Hydra::MessagingIO
33
+ # Creates a new uninitialized pipe pair.
34
+ def initialize
35
+ @child_read, @parent_write = IO.pipe
36
+ @parent_read, @child_write = IO.pipe
37
+ end
38
+
39
+ # Identify this side of the pipe as the child.
40
+ def identify_as_child
41
+ @parent_write.close
42
+ @parent_read.close
43
+ @reader = @child_read
44
+ @writer = @child_write
45
+ end
46
+
47
+ # Identify this side of the pipe as the parent
48
+ def identify_as_parent
49
+ @child_write.close
50
+ @child_read.close
51
+ @reader = @parent_read
52
+ @writer = @parent_write
53
+ end
54
+
55
+ # Output pipe nicely
56
+ def inspect
57
+ "#<#{self.class} @reader=#{@reader.to_s}, @writer=#{@writer.to_s}>"
58
+ end
59
+
60
+ end
61
+ end