ngauthier-hydra 0.24.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 (73) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.rdoc +51 -0
  6. data/Rakefile +8 -0
  7. data/TODO +18 -0
  8. data/hydra-icon-64x64.png +0 -0
  9. data/hydra.gemspec +24 -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/cucumber/partial_html.rb +24 -0
  14. data/lib/hydra/hash.rb +16 -0
  15. data/lib/hydra/js/lint.js +5150 -0
  16. data/lib/hydra/listener/abstract.rb +39 -0
  17. data/lib/hydra/listener/cucumber.css +279 -0
  18. data/lib/hydra/listener/cucumber_html_report.rb +148 -0
  19. data/lib/hydra/listener/jquery-min.js +154 -0
  20. data/lib/hydra/listener/minimal_output.rb +24 -0
  21. data/lib/hydra/listener/notifier.rb +17 -0
  22. data/lib/hydra/listener/progress_bar.rb +48 -0
  23. data/lib/hydra/listener/report_generator.rb +33 -0
  24. data/lib/hydra/master.rb +248 -0
  25. data/lib/hydra/message.rb +47 -0
  26. data/lib/hydra/message/master_messages.rb +19 -0
  27. data/lib/hydra/message/runner_messages.rb +46 -0
  28. data/lib/hydra/message/worker_messages.rb +52 -0
  29. data/lib/hydra/messaging_io.rb +49 -0
  30. data/lib/hydra/pipe.rb +61 -0
  31. data/lib/hydra/runner.rb +312 -0
  32. data/lib/hydra/runner_listener/abstract.rb +23 -0
  33. data/lib/hydra/safe_fork.rb +31 -0
  34. data/lib/hydra/spec/autorun_override.rb +3 -0
  35. data/lib/hydra/spec/hydra_formatter.rb +26 -0
  36. data/lib/hydra/ssh.rb +41 -0
  37. data/lib/hydra/stdio.rb +16 -0
  38. data/lib/hydra/sync.rb +99 -0
  39. data/lib/hydra/tasks.rb +375 -0
  40. data/lib/hydra/tmpdir.rb +11 -0
  41. data/lib/hydra/trace.rb +24 -0
  42. data/lib/hydra/version.rb +3 -0
  43. data/lib/hydra/worker.rb +170 -0
  44. data/test/fixtures/assert_true.rb +7 -0
  45. data/test/fixtures/config.yml +4 -0
  46. data/test/fixtures/conflicting.rb +10 -0
  47. data/test/fixtures/features/step_definitions.rb +21 -0
  48. data/test/fixtures/features/write_alternate_file.feature +7 -0
  49. data/test/fixtures/features/write_file.feature +7 -0
  50. data/test/fixtures/hello_world.rb +3 -0
  51. data/test/fixtures/hydra_worker_init.rb +2 -0
  52. data/test/fixtures/js_file.js +4 -0
  53. data/test/fixtures/json_data.json +4 -0
  54. data/test/fixtures/many_outputs_to_console.rb +9 -0
  55. data/test/fixtures/master_listeners.rb +10 -0
  56. data/test/fixtures/runner_listeners.rb +23 -0
  57. data/test/fixtures/slow.rb +9 -0
  58. data/test/fixtures/sync_test.rb +8 -0
  59. data/test/fixtures/task_test_config.yml +6 -0
  60. data/test/fixtures/write_file.rb +10 -0
  61. data/test/fixtures/write_file_alternate_spec.rb +10 -0
  62. data/test/fixtures/write_file_spec.rb +9 -0
  63. data/test/fixtures/write_file_with_pending_spec.rb +11 -0
  64. data/test/master_test.rb +383 -0
  65. data/test/message_test.rb +31 -0
  66. data/test/pipe_test.rb +38 -0
  67. data/test/runner_test.rb +196 -0
  68. data/test/ssh_test.rb +25 -0
  69. data/test/sync_test.rb +113 -0
  70. data/test/task_test.rb +21 -0
  71. data/test/test_helper.rb +107 -0
  72. data/test/worker_test.rb +60 -0
  73. metadata +229 -0
@@ -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,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,52 @@
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
+ class WorkerBegin < Hydra::Message
12
+ def handle(master, worker)
13
+ master.worker_begin(worker)
14
+ end
15
+ end
16
+
17
+ # Message telling the Runner to run a file
18
+ class RunFile < Hydra::Message
19
+ # The file that should be run
20
+ attr_accessor :file
21
+ def serialize #:nodoc:
22
+ super(:file => @file)
23
+ end
24
+ def handle(runner) #:nodoc:
25
+ runner.run_file(@file)
26
+ end
27
+ end
28
+
29
+ # Message to tell the Runner to shut down
30
+ class Shutdown < Hydra::Message
31
+ def handle(runner) #:nodoc:
32
+ runner.stop
33
+ end
34
+ end
35
+
36
+ # Message relaying the results of a worker up to the master
37
+ class Results < Hydra::Messages::Runner::Results
38
+ def handle(master, worker) #:nodoc:
39
+ master.process_results(worker, self)
40
+ end
41
+ end
42
+
43
+ # Message a worker sends to a master to verify the connection
44
+ class Ping < Hydra::Message
45
+ def handle(master, worker) #:nodoc:
46
+ # We don't do anything to handle a ping. It's just to test
47
+ # the connectivity of the IO
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,49 @@
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
+ while true
12
+ begin
13
+ raise IOError unless @reader
14
+ message = @reader.gets
15
+ return nil unless message
16
+ return Message.build(eval(message.chomp))
17
+ rescue SyntaxError, NameError
18
+ # uncomment to help catch remote errors by seeing all traffic
19
+ #$stderr.write "Not a message: [#{message.inspect}]\n"
20
+ end
21
+ end
22
+ end
23
+
24
+ # Write a Message to the output IO object. It will automatically
25
+ # serialize a Message object.
26
+ # IO.write Hydra::Message.new
27
+ def write(message)
28
+ raise IOError unless @writer
29
+ raise UnprocessableMessage unless message.is_a?(Hydra::Message)
30
+ @writer.write(message.serialize+"\n")
31
+ rescue Errno::EPIPE
32
+ raise IOError
33
+ end
34
+
35
+ # Closes the IO object.
36
+ def close
37
+ @reader.close if @reader
38
+ @writer.close if @writer
39
+ end
40
+
41
+ # IO will return this error if it cannot process a message.
42
+ # For example, if you tried to write a string, it would fail,
43
+ # because the string is not a message.
44
+ class UnprocessableMessage < RuntimeError
45
+ # Custom error message
46
+ attr_accessor :message
47
+ end
48
+ end
49
+ 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
@@ -0,0 +1,312 @@
1
+ require 'test/unit'
2
+ require 'test/unit/testresult'
3
+ Test::Unit.run = true
4
+
5
+ module Hydra #:nodoc:
6
+ # Hydra class responsible for running test files.
7
+ #
8
+ # The Runner is never run directly by a user. Runners are created by a
9
+ # Worker to run test files.
10
+ #
11
+ # The general convention is to have one Runner for each logical processor
12
+ # of a machine.
13
+ class Runner
14
+ include Hydra::Messages::Runner
15
+ traceable('RUNNER')
16
+
17
+ DEFAULT_LOG_FILE = 'hydra-runner.log'
18
+
19
+ # Boot up a runner. It takes an IO object (generally a pipe from its
20
+ # parent) to send it messages on which files to execute.
21
+ def initialize(opts = {})
22
+ redirect_output( opts.fetch( :runner_log_file ) { DEFAULT_LOG_FILE } )
23
+ reg_trap_sighup
24
+
25
+ @io = opts.fetch(:io) { raise "No IO Object" }
26
+ @verbose = opts.fetch(:verbose) { false }
27
+ @event_listeners = Array( opts.fetch( :runner_listeners ) { nil } )
28
+ @options = opts.fetch(:options) { "" }
29
+ @directory = get_directory
30
+
31
+ $stdout.sync = true
32
+ runner_begin
33
+
34
+ trace 'Booted. Sending Request for file'
35
+ @io.write RequestFile.new
36
+ begin
37
+ process_messages
38
+ rescue => ex
39
+ trace ex.to_s
40
+ raise ex
41
+ end
42
+ end
43
+
44
+ def reg_trap_sighup
45
+ for sign in [:SIGHUP, :INT]
46
+ trap sign do
47
+ stop
48
+ end
49
+ end
50
+ @runner_began = true
51
+ end
52
+
53
+ def runner_begin
54
+ trace "Firing runner_begin event"
55
+ @event_listeners.each {|l| l.runner_begin( self ) }
56
+ end
57
+
58
+ # Run a test file and report the results
59
+ def run_file(file)
60
+ trace "Running file: #{file}"
61
+
62
+ output = ""
63
+ if file =~ /_spec.rb$/i
64
+ output = run_rspec_file(file)
65
+ elsif file =~ /.feature$/i
66
+ output = run_cucumber_file(file)
67
+ elsif file =~ /.js$/i or file =~ /.json$/i
68
+ output = run_javascript_file(file)
69
+ else
70
+ output = run_test_unit_file(file)
71
+ end
72
+
73
+ output = "." if output == ""
74
+
75
+ @io.write Results.new(:output => output, :file => file)
76
+ return output
77
+ end
78
+
79
+ # Stop running
80
+ def stop
81
+ runner_end if @runner_began
82
+ @runner_began = @running = false
83
+ end
84
+
85
+ def runner_end
86
+ trace "Ending runner #{self.inspect}"
87
+ @event_listeners.each {|l| l.runner_end( self ) }
88
+ end
89
+
90
+ def format_exception(ex)
91
+ "#{ex.class.name}: #{ex.message}\n #{ex.backtrace.join("\n ")}"
92
+ end
93
+
94
+ private
95
+
96
+ # The runner will continually read messages and handle them.
97
+ def process_messages
98
+ trace "Processing Messages"
99
+ @running = true
100
+ while @running
101
+ begin
102
+ message = @io.gets
103
+ if message and !message.class.to_s.index("Worker").nil?
104
+ trace "Received message from worker"
105
+ trace "\t#{message.inspect}"
106
+ message.handle(self)
107
+ else
108
+ @io.write Ping.new
109
+ end
110
+ rescue IOError => ex
111
+ trace "Runner lost Worker"
112
+ stop
113
+ end
114
+ end
115
+ end
116
+
117
+ def format_ex_in_file(file, ex)
118
+ "Error in #{file}:\n #{format_exception(ex)}"
119
+ end
120
+
121
+ # Run all the Test::Unit Suites in a ruby file
122
+ def run_test_unit_file(file)
123
+ begin
124
+ require @directory + file
125
+ rescue LoadError => ex
126
+ trace "#{file} does not exist [#{ex.to_s}]"
127
+ return ex.to_s
128
+ rescue Exception => ex
129
+ trace "Error requiring #{file} [#{ex.to_s}]"
130
+ return format_ex_in_file(file, ex)
131
+ end
132
+ output = []
133
+ @result = Test::Unit::TestResult.new
134
+ @result.add_listener(Test::Unit::TestResult::FAULT) do |value|
135
+ output << value
136
+ end
137
+
138
+ klasses = Runner.find_classes_in_file(file)
139
+ begin
140
+ klasses.each{|klass| klass.suite.run(@result){|status, name| ;}}
141
+ rescue => ex
142
+ output << format_ex_in_file(file, ex)
143
+ end
144
+
145
+ return output.join("\n")
146
+ end
147
+
148
+ # run all the Specs in an RSpec file (NOT IMPLEMENTED)
149
+ def run_rspec_file(file)
150
+ # pull in rspec
151
+ begin
152
+ require 'rspec'
153
+ require 'hydra/spec/hydra_formatter'
154
+ # Ensure we override rspec's at_exit
155
+ RSpec::Core::Runner.disable_autorun!
156
+ rescue LoadError => ex
157
+ return ex.to_s
158
+ end
159
+ hydra_output = StringIO.new
160
+
161
+ config = [
162
+ '-f', 'RSpec::Core::Formatters::HydraFormatter',
163
+ file
164
+ ]
165
+
166
+ RSpec.instance_variable_set(:@world, nil)
167
+ RSpec::Core::Runner.run(config, hydra_output, hydra_output)
168
+
169
+ hydra_output.rewind
170
+ output = hydra_output.read.chomp
171
+ output = "" if output.gsub("\n","") =~ /^\.*$/
172
+
173
+ return output
174
+ end
175
+
176
+ # run all the scenarios in a cucumber feature file
177
+ def run_cucumber_file(file)
178
+ hydra_response = StringIO.new
179
+
180
+ options = @options if @options.is_a?(Array)
181
+ options = @options.split(' ') if @options.is_a?(String)
182
+
183
+ fork_id = fork do
184
+ files = [file]
185
+ dev_null = StringIO.new
186
+
187
+ args = [file, options].flatten.compact
188
+ hydra_response.puts args.inspect
189
+
190
+ results_directory = "#{Dir.pwd}/results/features"
191
+ FileUtils.mkdir_p results_directory
192
+
193
+ require 'cucumber/cli/main'
194
+ require 'hydra/cucumber/formatter'
195
+ require 'hydra/cucumber/partial_html'
196
+
197
+ Cucumber.logger.level = Logger::INFO
198
+
199
+ cuke = Cucumber::Cli::Main.new(args, dev_null, dev_null)
200
+ cuke.configuration.formats << ['Cucumber::Formatter::Hydra', hydra_response]
201
+
202
+ html_output = cuke.configuration.formats.select{|format| format[0] == 'html'}
203
+ if html_output
204
+ cuke.configuration.formats.delete(html_output)
205
+ cuke.configuration.formats << ['Hydra::Formatter::PartialHtml', "#{results_directory}/#{file.split('/').last}.html"]
206
+ end
207
+
208
+ cuke_runtime = Cucumber::Runtime.new(cuke.configuration)
209
+ cuke_runtime.run!
210
+ exit 1 if cuke_runtime.results.failure?
211
+ end
212
+ Process.wait fork_id
213
+
214
+ hydra_response.puts "." if not $?.exitstatus == 0
215
+ hydra_response.rewind
216
+
217
+ hydra_response.read
218
+ end
219
+
220
+ def run_javascript_file(file)
221
+ errors = []
222
+ require 'v8'
223
+ V8::Context.new do |context|
224
+ context.load(File.expand_path(File.join(File.dirname(__FILE__), 'js', 'lint.js')))
225
+ context['input'] = lambda{
226
+ File.read(file)
227
+ }
228
+ context['reportErrors'] = lambda{|js_errors|
229
+ js_errors.each do |e|
230
+ e = V8::To.rb(e)
231
+ errors << "\n\e[1;31mJSLINT: #{file}\e[0m"
232
+ errors << " Error at line #{e['line'].to_i + 1} " +
233
+ "character #{e['character'].to_i + 1}: \e[1;33m#{e['reason']}\e[0m"
234
+ errors << "#{e['evidence']}"
235
+ end
236
+ }
237
+ context.eval %{
238
+ JSLINT(input(), {
239
+ sub: true,
240
+ onevar: true,
241
+ eqeqeq: true,
242
+ plusplus: true,
243
+ bitwise: true,
244
+ regexp: true,
245
+ newcap: true,
246
+ immed: true,
247
+ strict: true,
248
+ rhino: true
249
+ });
250
+ reportErrors(JSLINT.errors);
251
+ }
252
+ end
253
+
254
+ if errors.empty?
255
+ return '.'
256
+ else
257
+ return errors.join("\n")
258
+ end
259
+ end
260
+
261
+ # find all the test unit classes in a given file, so we can run their suites
262
+ def self.find_classes_in_file(f)
263
+ code = ""
264
+ File.open(f) {|buffer| code = buffer.read}
265
+ matches = code.scan(/class\s+([\S]+)/)
266
+ klasses = matches.collect do |c|
267
+ begin
268
+ if c.first.respond_to? :constantize
269
+ c.first.constantize
270
+ else
271
+ eval(c.first)
272
+ end
273
+ rescue NameError
274
+ # means we could not load [c.first], but thats ok, its just not
275
+ # one of the classes we want to test
276
+ nil
277
+ rescue SyntaxError
278
+ # see above
279
+ nil
280
+ end
281
+ end
282
+ return klasses.select{|k| k.respond_to? 'suite'}
283
+ end
284
+
285
+ # Yanked a method from Cucumber
286
+ def tag_excess(features, limits)
287
+ limits.map do |tag_name, tag_limit|
288
+ tag_locations = features.tag_locations(tag_name)
289
+ if tag_limit && (tag_locations.length > tag_limit)
290
+ [tag_name, tag_limit, tag_locations]
291
+ else
292
+ nil
293
+ end
294
+ end.compact
295
+ end
296
+
297
+ def redirect_output file_name
298
+ begin
299
+ $stderr = $stdout = File.open(file_name, 'a')
300
+ rescue
301
+ # it should always redirect output in order to handle unexpected interruption
302
+ # successfully
303
+ $stderr = $stdout = File.open(DEFAULT_LOG_FILE, 'a')
304
+ end
305
+ end
306
+
307
+ def get_directory
308
+ RUBY_VERSION < "1.9" ? "" : Dir.pwd + "/"
309
+ end
310
+ end
311
+ end
312
+