arturop-hydra 0.23.4

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 (68) hide show
  1. data/.document +5 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +39 -0
  4. data/Rakefile +56 -0
  5. data/TODO +18 -0
  6. data/VERSION +1 -0
  7. data/caliper.yml +6 -0
  8. data/hydra-icon-64x64.png +0 -0
  9. data/hydra.gemspec +131 -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/hash.rb +16 -0
  14. data/lib/hydra/js/lint.js +5150 -0
  15. data/lib/hydra/listener/abstract.rb +39 -0
  16. data/lib/hydra/listener/minimal_output.rb +24 -0
  17. data/lib/hydra/listener/notifier.rb +17 -0
  18. data/lib/hydra/listener/progress_bar.rb +48 -0
  19. data/lib/hydra/listener/report_generator.rb +30 -0
  20. data/lib/hydra/master.rb +247 -0
  21. data/lib/hydra/message.rb +47 -0
  22. data/lib/hydra/message/master_messages.rb +19 -0
  23. data/lib/hydra/message/runner_messages.rb +46 -0
  24. data/lib/hydra/message/worker_messages.rb +52 -0
  25. data/lib/hydra/messaging_io.rb +49 -0
  26. data/lib/hydra/pipe.rb +61 -0
  27. data/lib/hydra/runner.rb +306 -0
  28. data/lib/hydra/runner_listener/abstract.rb +23 -0
  29. data/lib/hydra/safe_fork.rb +31 -0
  30. data/lib/hydra/spec/autorun_override.rb +3 -0
  31. data/lib/hydra/spec/hydra_formatter.rb +26 -0
  32. data/lib/hydra/ssh.rb +41 -0
  33. data/lib/hydra/stdio.rb +16 -0
  34. data/lib/hydra/sync.rb +99 -0
  35. data/lib/hydra/tasks.rb +366 -0
  36. data/lib/hydra/tmpdir.rb +11 -0
  37. data/lib/hydra/trace.rb +24 -0
  38. data/lib/hydra/worker.rb +168 -0
  39. data/test/fixtures/assert_true.rb +7 -0
  40. data/test/fixtures/config.yml +4 -0
  41. data/test/fixtures/conflicting.rb +10 -0
  42. data/test/fixtures/features/step_definitions.rb +21 -0
  43. data/test/fixtures/features/write_alternate_file.feature +7 -0
  44. data/test/fixtures/features/write_file.feature +7 -0
  45. data/test/fixtures/hello_world.rb +3 -0
  46. data/test/fixtures/hydra_worker_init.rb +2 -0
  47. data/test/fixtures/js_file.js +4 -0
  48. data/test/fixtures/json_data.json +4 -0
  49. data/test/fixtures/many_outputs_to_console.rb +9 -0
  50. data/test/fixtures/master_listeners.rb +10 -0
  51. data/test/fixtures/runner_listeners.rb +23 -0
  52. data/test/fixtures/slow.rb +9 -0
  53. data/test/fixtures/sync_test.rb +8 -0
  54. data/test/fixtures/task_test_config.yml +6 -0
  55. data/test/fixtures/write_file.rb +10 -0
  56. data/test/fixtures/write_file_alternate_spec.rb +10 -0
  57. data/test/fixtures/write_file_spec.rb +9 -0
  58. data/test/fixtures/write_file_with_pending_spec.rb +11 -0
  59. data/test/master_test.rb +383 -0
  60. data/test/message_test.rb +31 -0
  61. data/test/pipe_test.rb +38 -0
  62. data/test/runner_test.rb +196 -0
  63. data/test/ssh_test.rb +25 -0
  64. data/test/sync_test.rb +113 -0
  65. data/test/task_test.rb +21 -0
  66. data/test/test_helper.rb +107 -0
  67. data/test/worker_test.rb +60 -0
  68. metadata +202 -0
@@ -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,306 @@
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
+
29
+ $stdout.sync = true
30
+ runner_begin
31
+
32
+ trace 'Booted. Sending Request for file'
33
+ @io.write RequestFile.new
34
+ begin
35
+ process_messages
36
+ rescue => ex
37
+ trace ex.to_s
38
+ raise ex
39
+ end
40
+ end
41
+
42
+ def reg_trap_sighup
43
+ for sign in [:SIGHUP, :INT]
44
+ trap sign do
45
+ stop
46
+ end
47
+ end
48
+ @runner_began = true
49
+ end
50
+
51
+ def runner_begin
52
+ trace "Firing runner_begin event"
53
+ @event_listeners.each {|l| l.runner_begin( self ) }
54
+ end
55
+
56
+ # Run a test file and report the results
57
+ def run_file(file)
58
+ trace "Running file: #{file}"
59
+
60
+ output = ""
61
+ if file =~ /_spec.rb$/i
62
+ output = run_rspec_file(file)
63
+ elsif file =~ /.feature$/i
64
+ output = run_cucumber_file(file)
65
+ elsif file =~ /.js$/i or file =~ /.json$/i
66
+ output = run_javascript_file(file)
67
+ else
68
+ output = run_test_unit_file(file)
69
+ end
70
+
71
+ output = "." if output == ""
72
+
73
+ @io.write Results.new(:output => output, :file => file)
74
+ return output
75
+ end
76
+
77
+ # Stop running
78
+ def stop
79
+ runner_end if @runner_began
80
+ @runner_began = @running = false
81
+ end
82
+
83
+ def runner_end
84
+ trace "Ending runner #{self.inspect}"
85
+ @event_listeners.each {|l| l.runner_end( self ) }
86
+ end
87
+
88
+ def format_exception(ex)
89
+ "#{ex.class.name}: #{ex.message}\n #{ex.backtrace.join("\n ")}"
90
+ end
91
+
92
+ private
93
+
94
+ # The runner will continually read messages and handle them.
95
+ def process_messages
96
+ trace "Processing Messages"
97
+ @running = true
98
+ while @running
99
+ begin
100
+ message = @io.gets
101
+ if message and !message.class.to_s.index("Worker").nil?
102
+ trace "Received message from worker"
103
+ trace "\t#{message.inspect}"
104
+ message.handle(self)
105
+ else
106
+ @io.write Ping.new
107
+ end
108
+ rescue IOError => ex
109
+ trace "Runner lost Worker"
110
+ stop
111
+ end
112
+ end
113
+ end
114
+
115
+ def format_ex_in_file(file, ex)
116
+ "Error in #{file}:\n #{format_exception(ex)}"
117
+ end
118
+
119
+ # Run all the Test::Unit Suites in a ruby file
120
+ def run_test_unit_file(file)
121
+ begin
122
+ require file
123
+ rescue LoadError => ex
124
+ trace "#{file} does not exist [#{ex.to_s}]"
125
+ return ex.to_s
126
+ rescue Exception => ex
127
+ trace "Error requiring #{file} [#{ex.to_s}]"
128
+ return format_ex_in_file(file, ex)
129
+ end
130
+ output = []
131
+ @result = Test::Unit::TestResult.new
132
+ @result.add_listener(Test::Unit::TestResult::FAULT) do |value|
133
+ output << value
134
+ end
135
+
136
+ klasses = Runner.find_classes_in_file(file)
137
+ begin
138
+ klasses.each{|klass| klass.suite.run(@result){|status, name| ;}}
139
+ rescue => ex
140
+ output << format_ex_in_file(file, ex)
141
+ end
142
+
143
+ return output.join("\n")
144
+ end
145
+
146
+ # run all the Specs in an RSpec file (NOT IMPLEMENTED)
147
+ def run_rspec_file(file)
148
+ # pull in rspec
149
+ begin
150
+ require 'rspec'
151
+ require 'hydra/spec/hydra_formatter'
152
+ # Ensure we override rspec's at_exit
153
+ RSpec::Core::Runner.disable_autorun!
154
+ rescue LoadError => ex
155
+ return ex.to_s
156
+ end
157
+ hydra_output = StringIO.new
158
+
159
+ config = [
160
+ '-f', 'RSpec::Core::Formatters::HydraFormatter',
161
+ file
162
+ ]
163
+
164
+ RSpec.instance_variable_set(:@world, nil)
165
+ RSpec::Core::Runner.run(config, hydra_output, hydra_output)
166
+
167
+ hydra_output.rewind
168
+ output = hydra_output.read.chomp
169
+ output = "" if output.gsub("\n","") =~ /^\.*$/
170
+
171
+ return output
172
+ end
173
+
174
+ # run all the scenarios in a cucumber feature file
175
+ def run_cucumber_file(file)
176
+
177
+ files = [file]
178
+ dev_null = StringIO.new
179
+ hydra_response = StringIO.new
180
+
181
+ unless @cuke_runtime
182
+ require 'cucumber'
183
+ require 'hydra/cucumber/formatter'
184
+ Cucumber.logger.level = Logger::INFO
185
+ @cuke_runtime = Cucumber::Runtime.new
186
+ @cuke_configuration = Cucumber::Cli::Configuration.new(dev_null, dev_null)
187
+ @cuke_configuration.parse!(['features']+files)
188
+
189
+ support_code = Cucumber::Runtime::SupportCode.new(@cuke_runtime, @cuke_configuration.guess?)
190
+ support_code.load_files!(@cuke_configuration.support_to_load + @cuke_configuration.step_defs_to_load)
191
+ support_code.fire_hook(:after_configuration, @cuke_configuration)
192
+ # i don't like this, but there no access to set the instance of SupportCode in Runtime
193
+ @cuke_runtime.instance_variable_set('@support_code',support_code)
194
+ end
195
+ cuke_formatter = Cucumber::Formatter::Hydra.new(
196
+ @cuke_runtime, hydra_response, @cuke_configuration.options
197
+ )
198
+
199
+ cuke_runner ||= Cucumber::Ast::TreeWalker.new(
200
+ @cuke_runtime, [cuke_formatter], @cuke_configuration
201
+ )
202
+ @cuke_runtime.visitor = cuke_runner
203
+
204
+ loader = Cucumber::Runtime::FeaturesLoader.new(
205
+ files,
206
+ @cuke_configuration.filters,
207
+ @cuke_configuration.tag_expression
208
+ )
209
+ features = loader.features
210
+ tag_excess = tag_excess(features, @cuke_configuration.options[:tag_expression].limits)
211
+ @cuke_configuration.options[:tag_excess] = tag_excess
212
+
213
+ cuke_runner.visit_features(features)
214
+
215
+ hydra_response.rewind
216
+ return hydra_response.read
217
+ end
218
+
219
+ def run_javascript_file(file)
220
+ errors = []
221
+ require 'v8'
222
+ V8::Context.new do |context|
223
+ context.load(File.expand_path(File.join(File.dirname(__FILE__), 'js', 'lint.js')))
224
+ context['input'] = lambda{
225
+ File.read(file)
226
+ }
227
+ context['reportErrors'] = lambda{|js_errors|
228
+ js_errors.each do |e|
229
+ e = V8::To.rb(e)
230
+ errors << "\n\e[1;31mJSLINT: #{file}\e[0m"
231
+ errors << " Error at line #{e['line'].to_i + 1} " +
232
+ "character #{e['character'].to_i + 1}: \e[1;33m#{e['reason']}\e[0m"
233
+ errors << "#{e['evidence']}"
234
+ end
235
+ }
236
+ context.eval %{
237
+ JSLINT(input(), {
238
+ sub: true,
239
+ onevar: true,
240
+ eqeqeq: true,
241
+ plusplus: true,
242
+ bitwise: true,
243
+ regexp: true,
244
+ newcap: true,
245
+ immed: true,
246
+ strict: true,
247
+ rhino: true
248
+ });
249
+ reportErrors(JSLINT.errors);
250
+ }
251
+ end
252
+
253
+ if errors.empty?
254
+ return '.'
255
+ else
256
+ return errors.join("\n")
257
+ end
258
+ end
259
+
260
+ # find all the test unit classes in a given file, so we can run their suites
261
+ def self.find_classes_in_file(f)
262
+ code = ""
263
+ File.open(f) {|buffer| code = buffer.read}
264
+ matches = code.scan(/class\s+([\S]+)/)
265
+ klasses = matches.collect do |c|
266
+ begin
267
+ if c.first.respond_to? :constantize
268
+ c.first.constantize
269
+ else
270
+ eval(c.first)
271
+ end
272
+ rescue NameError
273
+ # means we could not load [c.first], but thats ok, its just not
274
+ # one of the classes we want to test
275
+ nil
276
+ rescue SyntaxError
277
+ # see above
278
+ nil
279
+ end
280
+ end
281
+ return klasses.select{|k| k.respond_to? 'suite'}
282
+ end
283
+
284
+ # Yanked a method from Cucumber
285
+ def tag_excess(features, limits)
286
+ limits.map do |tag_name, tag_limit|
287
+ tag_locations = features.tag_locations(tag_name)
288
+ if tag_limit && (tag_locations.length > tag_limit)
289
+ [tag_name, tag_limit, tag_locations]
290
+ else
291
+ nil
292
+ end
293
+ end.compact
294
+ end
295
+
296
+ def redirect_output file_name
297
+ begin
298
+ $stderr = $stdout = File.open(file_name, 'a')
299
+ rescue
300
+ # it should always redirect output in order to handle unexpected interruption
301
+ # successfully
302
+ $stderr = $stdout = File.open(DEFAULT_LOG_FILE, 'a')
303
+ end
304
+ end
305
+ end
306
+ end