sskirby-hydra 0.17.1 → 0.21.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,7 +2,7 @@ module Hydra #:nodoc:
2
2
  module Listener #:nodoc:
3
3
  # Abstract listener that implements all the events
4
4
  # but does nothing.
5
- class Abstract
5
+ class Abstract
6
6
  # Create a new listener.
7
7
  #
8
8
  # Output: The IO object for outputting any information.
@@ -10,14 +10,23 @@ module Hydra #:nodoc:
10
10
  def initialize(output = $stdout)
11
11
  @output = output
12
12
  end
13
+
13
14
  # Fired when testing has started
14
15
  def testing_begin(files)
15
16
  end
16
17
 
17
- # Fired when testing finishes
18
+ # Fired when testing finishes, after the workers shutdown
18
19
  def testing_end
19
20
  end
20
21
 
22
+ # Fired after runner processes have been started
23
+ def worker_begin(worker)
24
+ end
25
+
26
+ # Fired before shutting down the worker
27
+ def worker_end(worker)
28
+ end
29
+
21
30
  # Fired when a file is started
22
31
  def file_begin(file)
23
32
  end
@@ -34,7 +34,7 @@ module Hydra #:nodoc:
34
34
  complete = ((@files_completed.to_f / @total_files.to_f) * width).to_i
35
35
  @output.write "\r" # move to beginning
36
36
  @output.write 'Hydra Testing ['
37
- @output.write @errors ? "\033[1;31m" : "\033[1;32m"
37
+ @output.write @errors ? "\033[0;31m" : "\033[0;32m"
38
38
  complete.times{@output.write '#'}
39
39
  @output.write '>'
40
40
  (width-complete).times{@output.write ' '}
data/lib/hydra/master.rb CHANGED
@@ -1,11 +1,15 @@
1
1
  require 'hydra/hash'
2
2
  require 'open3'
3
3
  require 'tmpdir'
4
+ require 'erb'
4
5
  require 'yaml'
6
+
5
7
  module Hydra #:nodoc:
6
8
  # Hydra class responsible for delegate work down to workers.
7
9
  #
8
10
  # The Master is run once for any given testing session.
11
+ class YmlLoadError < StandardError; end
12
+
9
13
  class Master
10
14
  include Hydra::Messages::Master
11
15
  include Open3
@@ -29,16 +33,23 @@ module Hydra #:nodoc:
29
33
  # * :autosort
30
34
  # * Set to false to disable automatic sorting by historical run-time per file
31
35
  def initialize(opts = { })
32
- trap("SIGINT") do
33
- puts "Testing halted by user. Untested files:"
34
- puts @incomplete_files.join("\n")
35
- exit
36
- end
37
-
38
36
  opts.stringify_keys!
39
37
  config_file = opts.delete('config') { nil }
40
38
  if config_file
41
- opts.merge!(YAML.load_file(config_file).stringify_keys!)
39
+
40
+ begin
41
+ config_erb = ERB.new(IO.read(config_file)).result(binding)
42
+ rescue Exception => e
43
+ raise(YmlLoadError,"config file was found, but could not be parsed with ERB.\n#{$!.inspect}")
44
+ end
45
+
46
+ begin
47
+ config_yml = YAML::load(config_erb)
48
+ rescue StandardError => e
49
+ raise(YmlLoadError,"config file was found, but could not be parsed.\n#{$!.inspect}")
50
+ end
51
+
52
+ opts.merge!(config_yml.stringify_keys!)
42
53
  end
43
54
  @files = Array(opts.fetch('files') { nil })
44
55
  raise "No files, nothing to do" if @files.empty?
@@ -55,9 +66,10 @@ module Hydra #:nodoc:
55
66
  @autosort = opts.fetch('autosort') { true }
56
67
  @sync = opts.fetch('sync') { nil }
57
68
  @environment = opts.fetch('environment') { 'test' }
69
+ @remote_require = opts.fetch('remote_require') {[]}
58
70
 
59
71
  if @autosort
60
- sort_files_from_report
72
+ sort_files_from_report
61
73
  @event_listeners << Hydra::Listener::ReportGenerator.new(File.new(heuristic_file, 'w'))
62
74
  end
63
75
 
@@ -76,8 +88,11 @@ module Hydra #:nodoc:
76
88
  end
77
89
 
78
90
  # Message handling
79
-
80
- # Send a file down to a worker.
91
+ def worker_begin(worker)
92
+ @event_listeners.each {|l| l.worker_begin(worker) }
93
+ end
94
+
95
+ # Send a file down to a worker.
81
96
  def send_file(worker)
82
97
  f = @files.shift
83
98
  if f
@@ -93,14 +108,20 @@ module Hydra #:nodoc:
93
108
  def process_results(worker, message)
94
109
  if message.output =~ /ActiveRecord::StatementInvalid(.*)[Dd]eadlock/ or
95
110
  message.output =~ /PGError: ERROR(.*)[Dd]eadlock/ or
96
- message.output =~ /Mysql::Error: SAVEPOINT(.*)does not exist: ROLLBACK/
111
+ message.output =~ /Mysql::Error: SAVEPOINT(.*)does not exist: ROLLBACK/ or
112
+ message.output =~ /Mysql::Error: Deadlock found/
97
113
  trace "Deadlock detected running [#{message.file}]. Will retry at the end"
98
114
  @files.push(message.file)
115
+ send_file(worker)
99
116
  else
100
117
  @incomplete_files.delete_at(@incomplete_files.index(message.file))
101
118
  trace "#{@incomplete_files.size} Files Remaining"
102
119
  @event_listeners.each{|l| l.file_end(message.file, message.output) }
103
120
  if @incomplete_files.empty?
121
+ @workers.each do |worker|
122
+ @event_listeners.each{|l| l.worker_end(worker) }
123
+ end
124
+
104
125
  shutdown_all_workers
105
126
  else
106
127
  send_file(worker)
@@ -112,7 +133,7 @@ module Hydra #:nodoc:
112
133
  attr_reader :report_text
113
134
 
114
135
  private
115
-
136
+
116
137
  def boot_workers(workers)
117
138
  trace "Booting #{workers.size} workers"
118
139
  workers.each do |worker|
@@ -131,12 +152,13 @@ module Hydra #:nodoc:
131
152
 
132
153
  def boot_local_worker(worker)
133
154
  runners = worker.fetch('runners') { raise "You must specify the number of runners" }
134
- trace "Booting local worker"
155
+ trace "Booting local worker"
135
156
  pipe = Hydra::Pipe.new
136
157
  child = SafeFork.fork do
137
158
  pipe.identify_as_child
138
159
  Hydra::Worker.new(:io => pipe, :runners => runners, :verbose => @verbose)
139
160
  end
161
+
140
162
  pipe.identify_as_parent
141
163
  @workers << { :pid => child, :io => pipe, :idle => false, :type => :local }
142
164
  end
@@ -144,12 +166,14 @@ module Hydra #:nodoc:
144
166
  def boot_ssh_worker(worker)
145
167
  sync = Sync.new(worker, @sync, @verbose)
146
168
 
169
+ custom_require = @remote_require.map {|r| " require '#{r}';"}.join(' ')
170
+
147
171
  runners = worker.fetch('runners') { raise "You must specify the number of runners" }
148
- command = worker.fetch('command') {
149
- "RAILS_ENV=#{@environment} ruby -e \"require 'rubygems'; require 'hydra'; Hydra::Worker.new(:io => Hydra::Stdio.new, :runners => #{runners}, :verbose => #{@verbose});\""
172
+ command = worker.fetch('command') {
173
+ "RAILS_ENV=#{@environment} ruby -e \"require 'rubygems'; require 'hydra';#{custom_require} Hydra::Worker.new(:io => Hydra::Stdio.new, :runners => #{runners}, :verbose => #{@verbose}, :remote => '#{sync.connect}');\""
150
174
  }
151
175
 
152
- trace "Booting SSH worker"
176
+ trace "Booting SSH worker"
153
177
  ssh = Hydra::SSH.new("#{sync.ssh_opts} #{sync.connect}", sync.remote_dir, command)
154
178
  return { :io => ssh, :idle => false, :type => :ssh, :connect => sync.connect }
155
179
  end
@@ -158,7 +182,7 @@ module Hydra #:nodoc:
158
182
  trace "Shutting down all workers"
159
183
  @workers.each do |worker|
160
184
  worker[:io].write(Shutdown.new) if worker[:io]
161
- worker[:io].close if worker[:io]
185
+ worker[:io].close if worker[:io]
162
186
  end
163
187
  @listeners.each{|t| t.exit}
164
188
  end
@@ -182,7 +206,7 @@ module Hydra #:nodoc:
182
206
  # if it exists and its for me.
183
207
  # SSH gives us back echoes, so we need to ignore our own messages
184
208
  if message and !message.class.to_s.index("Worker").nil?
185
- message.handle(self, worker)
209
+ message.handle(self, worker)
186
210
  end
187
211
  rescue IOError
188
212
  trace "lost Worker [#{worker.inspect}]"
@@ -191,7 +215,7 @@ module Hydra #:nodoc:
191
215
  end
192
216
  end
193
217
  end
194
-
218
+
195
219
  @listeners.each{|l| l.join}
196
220
  @event_listeners.each{|l| l.testing_end}
197
221
  end
@@ -8,6 +8,12 @@ module Hydra #:nodoc:
8
8
  end
9
9
  end
10
10
 
11
+ class WorkerBegin < Hydra::Message
12
+ def handle(master, worker)
13
+ master.worker_begin(worker)
14
+ end
15
+ end
16
+
11
17
  # Message telling the Runner to run a file
12
18
  class RunFile < Hydra::Message
13
19
  # The file that should be run
@@ -11,6 +11,7 @@ module Hydra #:nodoc:
11
11
  raise IOError unless @reader
12
12
  message = @reader.gets
13
13
  return nil unless message
14
+ puts message if message.include?(Hydra::Trace::REMOTE_IDENTIFIER)
14
15
  return Message.build(eval(message.chomp))
15
16
  rescue SyntaxError, NameError
16
17
  # uncomment to help catch remote errors by seeing all traffic
data/lib/hydra/runner.rb CHANGED
@@ -18,6 +18,7 @@ module Hydra #:nodoc:
18
18
  def initialize(opts = {})
19
19
  @io = opts.fetch(:io) { raise "No IO Object" }
20
20
  @verbose = opts.fetch(:verbose) { false }
21
+ @remote = opts.fetch(:remote) { false }
21
22
  $stdout.sync = true
22
23
  trace 'Booted. Sending Request for file'
23
24
 
@@ -35,10 +36,12 @@ module Hydra #:nodoc:
35
36
  trace "Running file: #{file}"
36
37
 
37
38
  output = ""
38
- if file =~ /_spec.rb$/
39
+ if file =~ /_spec.rb$/i
39
40
  output = run_rspec_file(file)
40
- elsif file =~ /.feature$/
41
+ elsif file =~ /.feature$/i
41
42
  output = run_cucumber_file(file)
43
+ elsif file =~ /.js$/i or file =~ /.json$/i
44
+ output = run_javascript_file(file)
42
45
  else
43
46
  output = run_test_unit_file(file)
44
47
  end
@@ -54,6 +57,14 @@ module Hydra #:nodoc:
54
57
  @running = false
55
58
  end
56
59
 
60
+ def format_ex_in_file(file, ex)
61
+ "Error in #{file}:\n #{format_exception(ex)}"
62
+ end
63
+
64
+ def format_exception(ex)
65
+ "#{ex.class.name}: #{ex.message}\n #{ex.backtrace.join("\n ")}"
66
+ end
67
+
57
68
  private
58
69
 
59
70
  # The runner will continually read messages and handle them.
@@ -84,6 +95,9 @@ module Hydra #:nodoc:
84
95
  rescue LoadError => ex
85
96
  trace "#{file} does not exist [#{ex.to_s}]"
86
97
  return ex.to_s
98
+ rescue Exception => ex
99
+ trace "Error requiring #{file} [#{ex.to_s}]"
100
+ return format_ex_in_file(file, ex)
87
101
  end
88
102
  output = []
89
103
  @result = Test::Unit::TestResult.new
@@ -95,7 +109,7 @@ module Hydra #:nodoc:
95
109
  begin
96
110
  klasses.each{|klass| klass.suite.run(@result){|status, name| ;}}
97
111
  rescue => ex
98
- output << ex.to_s
112
+ output << format_ex_in_file(file, ex)
99
113
  end
100
114
 
101
115
  return output.join("\n")
@@ -105,7 +119,7 @@ module Hydra #:nodoc:
105
119
  def run_rspec_file(file)
106
120
  # pull in rspec
107
121
  begin
108
- require 'spec'
122
+ require 'rspec'
109
123
  require 'hydra/spec/hydra_formatter'
110
124
  # Ensure we override rspec's at_exit
111
125
  require 'hydra/spec/autorun_override'
@@ -113,22 +127,15 @@ module Hydra #:nodoc:
113
127
  return ex.to_s
114
128
  end
115
129
  hydra_output = StringIO.new
116
- Spec::Runner.options.instance_variable_set(:@formatters, [
117
- Spec::Runner::Formatter::HydraFormatter.new(
118
- Spec::Runner.options.formatter_options,
119
- hydra_output
120
- )
121
- ])
122
- Spec::Runner.options.instance_variable_set(
123
- :@example_groups, []
124
- )
125
- Spec::Runner.options.instance_variable_set(
126
- :@files, [file]
127
- )
128
- Spec::Runner.options.instance_variable_set(
129
- :@files_loaded, false
130
- )
131
- Spec::Runner.options.run_examples
130
+
131
+ config = [
132
+ '-f', 'RSpec::Core::Formatters::HydraFormatter',
133
+ file
134
+ ]
135
+
136
+ RSpec.instance_variable_set(:@world, nil)
137
+ RSpec::Core::Runner.run(config, hydra_output, hydra_output)
138
+
132
139
  hydra_output.rewind
133
140
  output = hydra_output.read.chomp
134
141
  output = "" if output.gsub("\n","") =~ /^\.*$/
@@ -175,6 +182,47 @@ module Hydra #:nodoc:
175
182
  return hydra_response.read
176
183
  end
177
184
 
185
+ def run_javascript_file(file)
186
+ errors = []
187
+ require 'v8'
188
+ V8::Context.new do |context|
189
+ context.load(File.expand_path(File.join(File.dirname(__FILE__), 'js', 'lint.js')))
190
+ context['input'] = lambda{
191
+ File.read(file)
192
+ }
193
+ context['reportErrors'] = lambda{|js_errors|
194
+ js_errors.each do |e|
195
+ e = V8::To.rb(e)
196
+ errors << "\n\e[1;31mJSLINT: #{file}\e[0m"
197
+ errors << " Error at line #{e['line'].to_i + 1} " +
198
+ "character #{e['character'].to_i + 1}: \e[1;33m#{e['reason']}\e[0m"
199
+ errors << "#{e['evidence']}"
200
+ end
201
+ }
202
+ context.eval %{
203
+ JSLINT(input(), {
204
+ sub: true,
205
+ onevar: true,
206
+ eqeqeq: true,
207
+ plusplus: true,
208
+ bitwise: true,
209
+ regexp: true,
210
+ newcap: true,
211
+ immed: true,
212
+ strict: true,
213
+ rhino: true
214
+ });
215
+ reportErrors(JSLINT.errors);
216
+ }
217
+ end
218
+
219
+ if errors.empty?
220
+ return '.'
221
+ else
222
+ return errors.join("\n")
223
+ end
224
+ end
225
+
178
226
  # find all the test unit classes in a given file, so we can run their suites
179
227
  def self.find_classes_in_file(f)
180
228
  code = ""
@@ -1,12 +1,3 @@
1
- if defined?(Spec)
2
- module Spec
3
- module Runner
4
- class << self
5
- # stop the auto-run at_exit
6
- def run
7
- return 0
8
- end
9
- end
10
- end
11
- end
1
+ if defined?(RSpec)
2
+ RSpec::Core::Runner.disable_autorun!
12
3
  end
@@ -1,14 +1,23 @@
1
- require 'spec/runner/formatter/progress_bar_formatter'
2
- module Spec
3
- module Runner
4
- module Formatter
5
- class HydraFormatter < ProgressBarFormatter
1
+ require 'rspec/core/formatters/progress_formatter'
2
+ module RSpec
3
+ module Core
4
+ module Formatters
5
+ class HydraFormatter < ProgressFormatter
6
+ def example_passed(example)
7
+ end
8
+
9
+ def example_pending(example)
10
+ end
11
+
12
+ def example_failed(example)
13
+ end
14
+
6
15
  # Stifle the post-test summary
7
16
  def dump_summary(duration, example, failure, pending)
8
17
  end
9
18
 
10
- # Stifle the output of pending examples
11
- def example_pending(*args)
19
+ # Stifle pending specs
20
+ def dump_pending
12
21
  end
13
22
  end
14
23
  end
data/lib/hydra/ssh.rb CHANGED
@@ -27,6 +27,7 @@ module Hydra #:nodoc:
27
27
  # list all the files.
28
28
  def initialize(connection_options, directory, command)
29
29
  @writer, @reader, @error = popen3("ssh -tt #{connection_options}")
30
+ @writer.write("mkdir -p #{directory}\n")
30
31
  @writer.write("cd #{directory}\n")
31
32
  @writer.write(command+"\n")
32
33
  end
data/lib/hydra/tasks.rb CHANGED
@@ -31,6 +31,16 @@ module Hydra #:nodoc:
31
31
  # t.listeners << Hydra::Listener::Notifier.new
32
32
  attr_accessor :listeners
33
33
 
34
+ # Set to true if you want to run this task only on the local
35
+ # machine with one runner. A "Safe Mode" for some test
36
+ # files that may not play nice with others.
37
+ attr_accessor :serial
38
+
39
+ # When running remote workers with extensions to hydra (such as
40
+ # custom messages), you will need to make those extensions
41
+ # available to the remote workers by specifying them here.
42
+ attr_accessor :remote_require
43
+
34
44
  #
35
45
  # Search for the hydra config file
36
46
  def find_config_file
@@ -40,7 +50,7 @@ module Hydra #:nodoc:
40
50
  return @config if File.exists?(@config)
41
51
  @config = nil
42
52
  end
43
-
53
+
44
54
  # Add files to test by passing in a string to be run through Dir.glob.
45
55
  # For example:
46
56
  #
@@ -59,7 +69,8 @@ module Hydra #:nodoc:
59
69
  # t.add_files 'test/integration/**/*_test.rb'
60
70
  # t.verbose = false # optionally set to true for lots of debug messages
61
71
  # t.autosort = false # disable automatic sorting based on runtime of tests
62
- # end
72
+ # t.remote_require << 'hydra_extensions'
73
+ # end
63
74
  class TestTask < Hydra::Task
64
75
 
65
76
  # Create a new HydraTestTask
@@ -68,20 +79,25 @@ module Hydra #:nodoc:
68
79
  @files = []
69
80
  @verbose = false
70
81
  @autosort = true
82
+ @serial = false
71
83
  @listeners = [Hydra::Listener::ProgressBar.new]
84
+ @remote_require = []
72
85
 
73
86
  yield self if block_given?
74
87
 
75
88
  # Ensure we override rspec's at_exit
76
89
  require 'hydra/spec/autorun_override'
77
90
 
78
- @config = find_config_file
91
+ unless @serial
92
+ @config = find_config_file
93
+ end
79
94
 
80
95
  @opts = {
81
96
  :verbose => @verbose,
82
97
  :autosort => @autosort,
83
98
  :files => @files,
84
- :listeners => @listeners
99
+ :listeners => @listeners,
100
+ :remote_require => @remote_require
85
101
  }
86
102
  if @config
87
103
  @opts.merge!(:config => @config)
@@ -97,6 +113,10 @@ module Hydra #:nodoc:
97
113
  def define
98
114
  desc "Hydra Tests" + (@name == :hydra ? "" : " for #{@name}")
99
115
  task @name do
116
+ if Object.const_defined?('RAILS_ENV') && RAILS_ENV == 'development'
117
+ $stderr.puts %{WARNING: RAILS_ENV is "development". Make sure to set it properly (ex: "RAILS_ENV=test rake hydra")}
118
+ end
119
+
100
120
  Hydra::Master.new(@opts)
101
121
  end
102
122
  end
@@ -302,7 +322,7 @@ module Hydra #:nodoc:
302
322
  # Hydra::GlobalTask.new('db:reset')
303
323
  #
304
324
  # Allows you to run:
305
- #
325
+ #
306
326
  # rake hydra:db:reset
307
327
  #
308
328
  # Then, db:reset will be run locally and on all remote workers. This
@@ -324,7 +344,7 @@ module Hydra #:nodoc:
324
344
  def define
325
345
  Hydra::RemoteTask.new(@name)
326
346
  desc "Run #{@name.to_s} Locally and Remotely across all Workers"
327
- task "hydra:#{@name.to_s}" => [@name.to_s, "hydra:remote:#{@name.to_s}"]
347
+ task "hydra:#{@name.to_s}" => [@name.to_s, "hydra:remote:#{@name.to_s}"]
328
348
  end
329
349
  end
330
350
  end