causes-hydra 0.21.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 (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