causes-hydra 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) 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/bin/warmsnake.rb +76 -0
  9. data/caliper.yml +6 -0
  10. data/hydra-icon-64x64.png +0 -0
  11. data/hydra.gemspec +130 -0
  12. data/hydra_gray.png +0 -0
  13. data/lib/hydra.rb +16 -0
  14. data/lib/hydra/cucumber/formatter.rb +29 -0
  15. data/lib/hydra/hash.rb +16 -0
  16. data/lib/hydra/js/lint.js +5150 -0
  17. data/lib/hydra/listener/abstract.rb +39 -0
  18. data/lib/hydra/listener/minimal_output.rb +24 -0
  19. data/lib/hydra/listener/notifier.rb +17 -0
  20. data/lib/hydra/listener/progress_bar.rb +48 -0
  21. data/lib/hydra/listener/report_generator.rb +30 -0
  22. data/lib/hydra/master.rb +249 -0
  23. data/lib/hydra/message.rb +47 -0
  24. data/lib/hydra/message/master_messages.rb +19 -0
  25. data/lib/hydra/message/runner_messages.rb +52 -0
  26. data/lib/hydra/message/worker_messages.rb +52 -0
  27. data/lib/hydra/messaging_io.rb +46 -0
  28. data/lib/hydra/pipe.rb +61 -0
  29. data/lib/hydra/runner.rb +305 -0
  30. data/lib/hydra/safe_fork.rb +31 -0
  31. data/lib/hydra/spec/autorun_override.rb +3 -0
  32. data/lib/hydra/spec/hydra_formatter.rb +26 -0
  33. data/lib/hydra/ssh.rb +41 -0
  34. data/lib/hydra/stdio.rb +16 -0
  35. data/lib/hydra/sync.rb +99 -0
  36. data/lib/hydra/tasks.rb +342 -0
  37. data/lib/hydra/trace.rb +24 -0
  38. data/lib/hydra/worker.rb +150 -0
  39. data/test/fixtures/assert_true.rb +7 -0
  40. data/test/fixtures/config.yml +4 -0
  41. data/test/fixtures/features/step_definitions.rb +21 -0
  42. data/test/fixtures/features/write_alternate_file.feature +7 -0
  43. data/test/fixtures/features/write_file.feature +7 -0
  44. data/test/fixtures/hello_world.rb +3 -0
  45. data/test/fixtures/js_file.js +4 -0
  46. data/test/fixtures/json_data.json +4 -0
  47. data/test/fixtures/slow.rb +9 -0
  48. data/test/fixtures/sync_test.rb +8 -0
  49. data/test/fixtures/write_file.rb +10 -0
  50. data/test/fixtures/write_file_alternate_spec.rb +10 -0
  51. data/test/fixtures/write_file_spec.rb +9 -0
  52. data/test/fixtures/write_file_with_pending_spec.rb +11 -0
  53. data/test/master_test.rb +152 -0
  54. data/test/message_test.rb +31 -0
  55. data/test/pipe_test.rb +38 -0
  56. data/test/runner_test.rb +153 -0
  57. data/test/ssh_test.rb +14 -0
  58. data/test/sync_test.rb +113 -0
  59. data/test/test_helper.rb +68 -0
  60. data/test/worker_test.rb +60 -0
  61. metadata +209 -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,52 @@
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
+ # Stats on the number of tests, assertions, errors and failures
18
+ attr_accessor :stats
19
+ def serialize #:nodoc:
20
+ super(
21
+ :output => @output,
22
+ :file => @file,
23
+ :stats => @stats
24
+ )
25
+ end
26
+ def handle(worker, runner) #:nodoc:
27
+ worker.relay_results(self, runner)
28
+ end
29
+ end
30
+
31
+ # Message a runner sends to a worker to verify the connection
32
+ class Ping < Hydra::Message
33
+ def handle(worker, runner) #:nodoc:
34
+ # We don't do anything to handle a ping. It's just to test
35
+ # the connectivity of the IO
36
+ end
37
+ end
38
+
39
+ # The runner forks to run rspec messages
40
+ # so that specs don't get rerun. It uses
41
+ # this message to report the results. See
42
+ # Runner::run_rspec_file.
43
+ class RSpecResult < Hydra::Message
44
+ # the output of the spec
45
+ attr_accessor :output
46
+ def serialize #:nodoc:
47
+ super(:output => @output)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ 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,305 @@
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
+ $stdout.sync = true
22
+ trace 'Booted. Sending Request for file'
23
+
24
+ @io.write RequestFile.new
25
+ begin
26
+ process_messages
27
+ rescue => ex
28
+ trace ex.to_s
29
+ raise ex
30
+ end
31
+ end
32
+
33
+ # Run a test file and report the results
34
+ def run_file(file)
35
+ trace "Running file: #{file}"
36
+
37
+ output = ""
38
+ if file =~ /_spec.rb$/i
39
+ output, stats = run_rspec_file(file)
40
+ elsif file =~ /_testspec.rb$/i
41
+ output, stats = run_test_spec_file(file)
42
+ elsif file =~ /.feature$/i
43
+ output, stats = run_cucumber_file(file)
44
+ elsif file =~ /.js$/i or file =~ /.json$/i
45
+ output, stats = run_javascript_file(file)
46
+ else
47
+ output, stats = run_test_unit_file(file)
48
+ end
49
+
50
+ output = "." if output == ""
51
+
52
+ @io.write Results.new(
53
+ :output => output,
54
+ :file => file,
55
+ :stats => stats
56
+ )
57
+ return output
58
+ end
59
+
60
+ # Stop running
61
+ def stop
62
+ @running = false
63
+ end
64
+
65
+ private
66
+
67
+ # The runner will continually read messages and handle them.
68
+ def process_messages
69
+ trace "Processing Messages"
70
+ @running = true
71
+ while @running
72
+ begin
73
+ message = @io.gets
74
+ if message and !message.class.to_s.index("Worker").nil?
75
+ trace "Received message from worker"
76
+ trace "\t#{message.inspect}"
77
+ message.handle(self)
78
+ else
79
+ @io.write Ping.new
80
+ end
81
+ rescue IOError => ex
82
+ trace "Runner lost Worker"
83
+ @running = false
84
+ end
85
+ end
86
+ end
87
+
88
+ # Run all the Test::Unit Suites in a ruby file
89
+ def run_test_unit_file(file)
90
+ begin
91
+ require file
92
+ rescue LoadError => ex
93
+ trace "#{file} does not exist [#{ex.to_s}]"
94
+ return ex.to_s
95
+ end
96
+ output = []
97
+ @result = Test::Unit::TestResult.new
98
+ @result.add_listener(Test::Unit::TestResult::FAULT) do |value|
99
+ output << `hostname`.chomp
100
+ output << value
101
+ end
102
+
103
+ klasses = Runner.find_classes_in_file(file)
104
+ begin
105
+ klasses.each{|klass| klass.suite.run(@result){|status, name| ;}}
106
+ rescue => ex
107
+ output << `hostname`.chomp
108
+ output << ex.to_s
109
+ end
110
+ stats = {
111
+ :tests => @result.run_count,
112
+ :assertions => @result.assertion_count,
113
+ :failures => @result.failure_count,
114
+ :errors => @result.error_count,
115
+ }
116
+
117
+ return output.join("\n"), stats
118
+ end
119
+
120
+ # Run all the test/spec file
121
+ def run_test_spec_file(file)
122
+ begin
123
+ require file
124
+ rescue LoadError => ex
125
+ trace "#{file} does not exist [#{ex.to_s}]"
126
+ return ex.to_s
127
+ end
128
+ output = []
129
+ @result = Test::Unit::TestResult.new
130
+ @result.add_listener(Test::Unit::TestResult::FAULT) do |value|
131
+ output << `hostname`.chomp
132
+ output << value
133
+ end
134
+
135
+ klasses = Runner.find_test_spec_classes_in_file(file)
136
+ begin
137
+ klasses.each{|klass| klass.suite.run(@result){|status, name| ;}}
138
+ rescue => ex
139
+ output << `hostname`.chomp
140
+ output << ex.to_s
141
+ end
142
+
143
+ stats = {
144
+ :tests => @result.run_count,
145
+ :assertions => @result.assertion_count,
146
+ :failures => @result.failure_count,
147
+ :errors => @result.error_count,
148
+ }
149
+
150
+ return output.join("\n"), stats
151
+ end
152
+
153
+ # run all the Specs in an RSpec file (NOT IMPLEMENTED)
154
+ def run_rspec_file(file)
155
+ # pull in rspec
156
+ begin
157
+ require 'rspec'
158
+ require 'hydra/spec/hydra_formatter'
159
+ # Ensure we override rspec's at_exit
160
+ require 'hydra/spec/autorun_override'
161
+ rescue LoadError => ex
162
+ return ex.to_s
163
+ end
164
+ hydra_output = StringIO.new
165
+
166
+ config = [
167
+ '-f', 'RSpec::Core::Formatters::HydraFormatter',
168
+ file
169
+ ]
170
+
171
+ RSpec.instance_variable_set(:@world, nil)
172
+ RSpec::Core::Runner.run(config, hydra_output, hydra_output)
173
+
174
+ hydra_output.rewind
175
+ output = hydra_output.read.chomp
176
+ output = "" if output.gsub("\n","") =~ /^\.*$/
177
+
178
+ return output
179
+ end
180
+
181
+ # run all the scenarios in a cucumber feature file
182
+ def run_cucumber_file(file)
183
+
184
+ files = [file]
185
+ dev_null = StringIO.new
186
+ hydra_response = StringIO.new
187
+
188
+ unless @step_mother
189
+ require 'cucumber'
190
+ require 'hydra/cucumber/formatter'
191
+ @step_mother = Cucumber::StepMother.new
192
+ @cuke_configuration = Cucumber::Cli::Configuration.new(dev_null, dev_null)
193
+ @cuke_configuration.parse!(['features']+files)
194
+
195
+ @step_mother.options = @cuke_configuration.options
196
+ @step_mother.log = @cuke_configuration.log
197
+ @step_mother.load_code_files(@cuke_configuration.support_to_load)
198
+ @step_mother.after_configuration(@cuke_configuration)
199
+ @step_mother.load_code_files(@cuke_configuration.step_defs_to_load)
200
+ end
201
+ cuke_formatter = Cucumber::Formatter::Hydra.new(
202
+ @step_mother, hydra_response, @cuke_configuration.options
203
+ )
204
+
205
+ cuke_runner ||= Cucumber::Ast::TreeWalker.new(
206
+ @step_mother, [cuke_formatter], @cuke_configuration.options, dev_null
207
+ )
208
+ @step_mother.visitor = cuke_runner
209
+
210
+ features = @step_mother.load_plain_text_features(files)
211
+ tag_excess = tag_excess(features, @cuke_configuration.options[:tag_expression].limits)
212
+ @cuke_configuration.options[:tag_excess] = tag_excess
213
+
214
+ cuke_runner.visit_features(features)
215
+
216
+ hydra_response.rewind
217
+ return 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
+ def self.find_test_spec_classes_in_file(f)
262
+ require f
263
+ ks = Test::Spec::CONTEXTS.values.map{|k| k.testcase}
264
+ Test::Spec::CONTEXTS.clear
265
+ Test::Spec::SHARED_CONTEXTS.clear
266
+ ks
267
+ end
268
+
269
+ # find all the test unit classes in a given file, so we can run their suites
270
+ def self.find_classes_in_file(f)
271
+ code = ""
272
+ File.open(f) {|buffer| code = buffer.read}
273
+ matches = code.scan(/class\s+([\S]+)/)
274
+ klasses = matches.collect do |c|
275
+ begin
276
+ if c.first.respond_to? :constantize
277
+ c.first.constantize
278
+ else
279
+ eval(c.first)
280
+ end
281
+ rescue NameError
282
+ # means we could not load [c.first], but thats ok, its just not
283
+ # one of the classes we want to test
284
+ nil
285
+ rescue SyntaxError
286
+ # see above
287
+ nil
288
+ end
289
+ end
290
+ return klasses.select{|k| k.respond_to? 'suite'}
291
+ end
292
+
293
+ # Yanked a method from Cucumber
294
+ def tag_excess(features, limits)
295
+ limits.map do |tag_name, tag_limit|
296
+ tag_locations = features.tag_locations(tag_name)
297
+ if tag_limit && (tag_locations.length > tag_limit)
298
+ [tag_name, tag_limit, tag_locations]
299
+ else
300
+ nil
301
+ end
302
+ end.compact
303
+ end
304
+ end
305
+ end