sskirby-hydra 0.16.9

Sign up to get free protection for your applications and to get access to all the features.
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