sskirby-hydra 0.17.1 → 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.
@@ -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