arturop-hydra 0.23.4

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