bellmyer-hydra 0.20.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) 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 +56 -0
  6. data/TODO +18 -0
  7. data/VERSION +1 -0
  8. data/bellmyer-hydra.gemspec +128 -0
  9. data/caliper.yml +6 -0
  10. data/hydra-icon-64x64.png +0 -0
  11. data/hydra_gray.png +0 -0
  12. data/lib/hydra.rb +16 -0
  13. data/lib/hydra/cucumber/formatter.rb +30 -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/minimal_output.rb +24 -0
  18. data/lib/hydra/listener/notifier.rb +17 -0
  19. data/lib/hydra/listener/progress_bar.rb +48 -0
  20. data/lib/hydra/listener/report_generator.rb +30 -0
  21. data/lib/hydra/master.rb +238 -0
  22. data/lib/hydra/message.rb +47 -0
  23. data/lib/hydra/message/master_messages.rb +19 -0
  24. data/lib/hydra/message/runner_messages.rb +46 -0
  25. data/lib/hydra/message/worker_messages.rb +52 -0
  26. data/lib/hydra/messaging_io.rb +46 -0
  27. data/lib/hydra/pipe.rb +61 -0
  28. data/lib/hydra/runner.rb +265 -0
  29. data/lib/hydra/safe_fork.rb +31 -0
  30. data/lib/hydra/spec/autorun_override.rb +12 -0
  31. data/lib/hydra/spec/hydra_formatter.rb +17 -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 +342 -0
  36. data/lib/hydra/trace.rb +24 -0
  37. data/lib/hydra/worker.rb +150 -0
  38. data/test/fixtures/assert_true.rb +7 -0
  39. data/test/fixtures/config.yml +4 -0
  40. data/test/fixtures/features/step_definitions.rb +21 -0
  41. data/test/fixtures/features/write_alternate_file.feature +7 -0
  42. data/test/fixtures/features/write_file.feature +7 -0
  43. data/test/fixtures/hello_world.rb +3 -0
  44. data/test/fixtures/js_file.js +4 -0
  45. data/test/fixtures/json_data.json +4 -0
  46. data/test/fixtures/slow.rb +9 -0
  47. data/test/fixtures/sync_test.rb +8 -0
  48. data/test/fixtures/write_file.rb +10 -0
  49. data/test/fixtures/write_file_alternate_spec.rb +10 -0
  50. data/test/fixtures/write_file_spec.rb +9 -0
  51. data/test/fixtures/write_file_with_pending_spec.rb +11 -0
  52. data/test/master_test.rb +152 -0
  53. data/test/message_test.rb +31 -0
  54. data/test/pipe_test.rb +38 -0
  55. data/test/runner_test.rb +156 -0
  56. data/test/ssh_test.rb +14 -0
  57. data/test/sync_test.rb +113 -0
  58. data/test/test_helper.rb +68 -0
  59. data/test/worker_test.rb +60 -0
  60. metadata +208 -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,46 @@
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
+ raise IOError
30
+ end
31
+
32
+ # Closes the IO object.
33
+ def close
34
+ @reader.close if @reader
35
+ @writer.close if @writer
36
+ end
37
+
38
+ # IO will return this error if it cannot process a message.
39
+ # For example, if you tried to write a string, it would fail,
40
+ # because the string is not a message.
41
+ class UnprocessableMessage < RuntimeError
42
+ # Custom error message
43
+ attr_accessor :message
44
+ end
45
+ end
46
+ end
@@ -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,265 @@
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
+ # Boot up a runner. It takes an IO object (generally a pipe from its
17
+ # parent) to send it messages on which files to execute.
18
+ def initialize(opts = {})
19
+ @io = opts.fetch(:io) { raise "No IO Object" }
20
+ @verbose = opts.fetch(:verbose) { false }
21
+
22
+ @worker_id = opts.fetch(:worker_id).to_s
23
+ @worker_id = '' if @worker_id == '0'
24
+
25
+ $stdout.sync = true
26
+
27
+ trace 'Creating test database'
28
+ ENV['TEST_ENV_NUMBER'] = @worker_id
29
+
30
+ trace 'Booted. Sending Request for file'
31
+
32
+ @io.write RequestFile.new
33
+ begin
34
+ process_messages
35
+ rescue => ex
36
+ trace ex.to_s
37
+ raise ex
38
+ end
39
+ end
40
+
41
+ # Run a test file and report the results
42
+ def run_file(file)
43
+ trace "Running file: #{file}"
44
+
45
+ output = ""
46
+ if file =~ /_spec.rb$/i
47
+ output = run_rspec_file(file)
48
+ elsif file =~ /.feature$/i
49
+ output = run_cucumber_file(file)
50
+ elsif file =~ /.js$/i or file =~ /.json$/i
51
+ output = run_javascript_file(file)
52
+ else
53
+ output = run_test_unit_file(file)
54
+ end
55
+
56
+ output = "." if output == ""
57
+
58
+ @io.write Results.new(:output => output, :file => file)
59
+ return output
60
+ end
61
+
62
+ # Stop running
63
+ def stop
64
+ @running = false
65
+ end
66
+
67
+ private
68
+
69
+ # The runner will continually read messages and handle them.
70
+ def process_messages
71
+ trace "Processing Messages"
72
+ @running = true
73
+ while @running
74
+ begin
75
+ message = @io.gets
76
+ if message and !message.class.to_s.index("Worker").nil?
77
+ trace "Received message from worker"
78
+ trace "\t#{message.inspect}"
79
+ message.handle(self)
80
+ else
81
+ @io.write Ping.new
82
+ end
83
+ rescue IOError => ex
84
+ trace "Runner lost Worker"
85
+ @running = false
86
+ end
87
+ end
88
+ end
89
+
90
+ # Run all the Test::Unit Suites in a ruby file
91
+ def run_test_unit_file(file)
92
+ begin
93
+ require file
94
+ rescue LoadError => ex
95
+ trace "#{file} does not exist [#{ex.to_s}]"
96
+ return ex.to_s
97
+ end
98
+ output = []
99
+ @result = Test::Unit::TestResult.new
100
+ @result.add_listener(Test::Unit::TestResult::FAULT) do |value|
101
+ output << value
102
+ end
103
+
104
+ klasses = Runner.find_classes_in_file(file)
105
+ begin
106
+ klasses.each{|klass| klass.suite.run(@result){|status, name| ;}}
107
+ rescue => ex
108
+ output << ex.to_s
109
+ end
110
+
111
+ return output.join("\n")
112
+ end
113
+
114
+ # run all the Specs in an RSpec file (NOT IMPLEMENTED)
115
+ def run_rspec_file(file)
116
+ # pull in rspec
117
+ begin
118
+ require 'spec'
119
+ require 'hydra/spec/hydra_formatter'
120
+ # Ensure we override rspec's at_exit
121
+ require 'hydra/spec/autorun_override'
122
+ rescue LoadError => ex
123
+ return ex.to_s
124
+ end
125
+ hydra_output = StringIO.new
126
+ Spec::Runner.options.instance_variable_set(:@formatters, [
127
+ Spec::Runner::Formatter::HydraFormatter.new(
128
+ Spec::Runner.options.formatter_options,
129
+ hydra_output
130
+ )
131
+ ])
132
+ Spec::Runner.options.instance_variable_set(
133
+ :@example_groups, []
134
+ )
135
+ Spec::Runner.options.instance_variable_set(
136
+ :@files, [file]
137
+ )
138
+ Spec::Runner.options.instance_variable_set(
139
+ :@files_loaded, false
140
+ )
141
+ Spec::Runner.options.run_examples
142
+ hydra_output.rewind
143
+ output = hydra_output.read.chomp
144
+ output = "" if output.gsub("\n","") =~ /^\.*$/
145
+
146
+ return output
147
+ end
148
+
149
+ # run all the scenarios in a cucumber feature file
150
+ def run_cucumber_file(file)
151
+
152
+ files = [file]
153
+ dev_null = StringIO.new
154
+ hydra_response = StringIO.new
155
+
156
+ unless @step_mother
157
+ require 'cucumber'
158
+ require 'hydra/cucumber/formatter'
159
+ @step_mother = Cucumber::StepMother.new
160
+ @cuke_configuration = Cucumber::Cli::Configuration.new(dev_null, dev_null)
161
+ @cuke_configuration.parse!(['features']+files)
162
+
163
+ @step_mother.options = @cuke_configuration.options
164
+ @step_mother.log = @cuke_configuration.log
165
+ @step_mother.load_code_files(@cuke_configuration.support_to_load)
166
+ @step_mother.after_configuration(@cuke_configuration)
167
+ @step_mother.load_code_files(@cuke_configuration.step_defs_to_load)
168
+ end
169
+ cuke_formatter = Cucumber::Formatter::Hydra.new(
170
+ @step_mother, hydra_response, @cuke_configuration.options
171
+ )
172
+
173
+ cuke_runner ||= Cucumber::Ast::TreeWalker.new(
174
+ @step_mother, [cuke_formatter], @cuke_configuration.options, dev_null
175
+ )
176
+ @step_mother.visitor = cuke_runner
177
+
178
+ features = @step_mother.load_plain_text_features(files)
179
+ tag_excess = tag_excess(features, @cuke_configuration.options[:tag_expression].limits)
180
+ @cuke_configuration.options[:tag_excess] = tag_excess
181
+
182
+ cuke_runner.visit_features(features)
183
+
184
+ hydra_response.rewind
185
+ return hydra_response.read
186
+ end
187
+
188
+ def run_javascript_file(file)
189
+ errors = []
190
+ require 'v8'
191
+ V8::Context.new do |context|
192
+ context.load(File.expand_path(File.join(File.dirname(__FILE__), 'js', 'lint.js')))
193
+ context['input'] = lambda{
194
+ File.read(file)
195
+ }
196
+ context['reportErrors'] = lambda{|js_errors|
197
+ js_errors.each do |e|
198
+ e = V8::To.rb(e)
199
+ errors << "\n\e[1;31mJSLINT: #{file}\e[0m"
200
+ errors << " Error at line #{e['line'].to_i + 1} " +
201
+ "character #{e['character'].to_i + 1}: \e[1;33m#{e['reason']}\e[0m"
202
+ errors << "#{e['evidence']}"
203
+ end
204
+ }
205
+ context.eval %{
206
+ JSLINT(input(), {
207
+ sub: true,
208
+ onevar: true,
209
+ eqeqeq: true,
210
+ plusplus: true,
211
+ bitwise: true,
212
+ regexp: true,
213
+ newcap: true,
214
+ immed: true,
215
+ strict: true,
216
+ rhino: true
217
+ });
218
+ reportErrors(JSLINT.errors);
219
+ }
220
+ end
221
+
222
+ if errors.empty?
223
+ return '.'
224
+ else
225
+ return errors.join("\n")
226
+ end
227
+ end
228
+
229
+ # find all the test unit classes in a given file, so we can run their suites
230
+ def self.find_classes_in_file(f)
231
+ code = ""
232
+ File.open(f) {|buffer| code = buffer.read}
233
+ matches = code.scan(/class\s+([\S]+)/)
234
+ klasses = matches.collect do |c|
235
+ begin
236
+ if c.first.respond_to? :constantize
237
+ c.first.constantize
238
+ else
239
+ eval(c.first)
240
+ end
241
+ rescue NameError
242
+ # means we could not load [c.first], but thats ok, its just not
243
+ # one of the classes we want to test
244
+ nil
245
+ rescue SyntaxError
246
+ # see above
247
+ nil
248
+ end
249
+ end
250
+ return klasses.select{|k| k.respond_to? 'suite'}
251
+ end
252
+
253
+ # Yanked a method from Cucumber
254
+ def tag_excess(features, limits)
255
+ limits.map do |tag_name, tag_limit|
256
+ tag_locations = features.tag_locations(tag_name)
257
+ if tag_limit && (tag_locations.length > tag_limit)
258
+ [tag_name, tag_limit, tag_locations]
259
+ else
260
+ nil
261
+ end
262
+ end.compact
263
+ end
264
+ end
265
+ end