305 lines
8.3 KiB
Ruby
305 lines
8.3 KiB
Ruby
module Hydra #:nodoc:
|
|
# Hydra class responsible for running test files.
|
|
#
|
|
# The Runner is never run directly by a user. Runners are created by a
|
|
# Worker to run test files.
|
|
#
|
|
# The general convention is to have one Runner for each logical processor
|
|
# of a machine.
|
|
class Runner
|
|
include Hydra::Messages::Runner
|
|
traceable('RUNNER')
|
|
|
|
DEFAULT_LOG_FILE = 'hydra-runner.log'
|
|
|
|
# Boot up a runner. It takes an IO object (generally a pipe from its
|
|
# parent) to send it messages on which files to execute.
|
|
def initialize(opts = {})
|
|
#redirect_output( opts.fetch( :runner_log_file ) { DEFAULT_LOG_FILE } )
|
|
reg_trap_sighup
|
|
|
|
@io = opts.fetch(:io) { raise "No IO Object" }
|
|
@verbose = opts.fetch(:verbose) { false }
|
|
@event_listeners = Array( opts.fetch( :runner_listeners ) { nil } )
|
|
@options = opts.fetch(:options)
|
|
|
|
$stdout.sync = true
|
|
runner_begin
|
|
|
|
trace 'Booted. Sending Request for file'
|
|
@io.write RequestFile.new
|
|
begin
|
|
process_messages
|
|
rescue => ex
|
|
trace ex.to_s
|
|
raise ex
|
|
end
|
|
end
|
|
|
|
def reg_trap_sighup
|
|
for sign in [:SIGHUP, :INT]
|
|
trap sign do
|
|
stop
|
|
end
|
|
end
|
|
@runner_began = true
|
|
end
|
|
|
|
def runner_begin
|
|
trace "Firing runner_begin event"
|
|
@event_listeners.each {|l| l.runner_begin( self ) }
|
|
end
|
|
|
|
# Run a test file and report the results
|
|
def run_file(file)
|
|
trace "Running file: #{file}"
|
|
|
|
output = ""
|
|
if file =~ /_spec.rb$/i
|
|
output = run_rspec_file(file)
|
|
elsif file =~ /.feature$/i
|
|
output = run_cucumber_file(file)
|
|
elsif file =~ /.js$/i or file =~ /.json$/i
|
|
output = run_javascript_file(file)
|
|
else
|
|
output = run_test_unit_file(file)
|
|
end
|
|
|
|
output = "." if output == ""
|
|
|
|
@io.write Results.new(:output => output, :file => file)
|
|
return output
|
|
end
|
|
|
|
# Stop running
|
|
def stop
|
|
runner_end if @runner_began
|
|
@runner_began = @running = false
|
|
end
|
|
|
|
def runner_end
|
|
trace "Ending runner #{self.inspect}"
|
|
@event_listeners.each {|l| l.runner_end( self ) }
|
|
end
|
|
|
|
def format_exception(ex)
|
|
"#{ex.class.name}: #{ex.message}\n #{ex.backtrace.join("\n ")}"
|
|
end
|
|
|
|
private
|
|
|
|
# The runner will continually read messages and handle them.
|
|
def process_messages
|
|
trace "Processing Messages"
|
|
@running = true
|
|
while @running
|
|
begin
|
|
message = @io.gets
|
|
if message and !message.class.to_s.index("Worker").nil?
|
|
trace "Received message from worker"
|
|
trace "\t#{message.inspect}"
|
|
message.handle(self)
|
|
else
|
|
@io.write Ping.new
|
|
end
|
|
rescue IOError => ex
|
|
trace "Runner lost Worker"
|
|
stop
|
|
end
|
|
end
|
|
end
|
|
|
|
def format_ex_in_file(file, ex)
|
|
"Error in #{file}:\n #{format_exception(ex)}"
|
|
end
|
|
|
|
# Run all the Test::Unit Suites in a ruby file
|
|
def run_test_unit_file(file)
|
|
begin
|
|
require 'test/unit'
|
|
require 'test/unit/testresult'
|
|
Test::Unit.run = true
|
|
require file
|
|
rescue LoadError => ex
|
|
trace "#{file} does not exist [#{ex.to_s}]"
|
|
return ex.to_s
|
|
rescue Exception => ex
|
|
trace "Error requiring #{file} [#{ex.to_s}]"
|
|
return format_ex_in_file(file, ex)
|
|
end
|
|
output = []
|
|
@result = Test::Unit::TestResult.new
|
|
@result.add_listener(Test::Unit::TestResult::FAULT) do |value|
|
|
output << value
|
|
end
|
|
|
|
klasses = Runner.find_classes_in_file(file)
|
|
begin
|
|
klasses.each{|klass| klass.suite.run(@result){|status, name| ;}}
|
|
rescue => ex
|
|
output << format_ex_in_file(file, ex)
|
|
end
|
|
|
|
return output.join("\n")
|
|
end
|
|
|
|
# run all the Specs in an RSpec file (NOT IMPLEMENTED)
|
|
def run_rspec_file(file)
|
|
# pull in rspec
|
|
begin
|
|
require 'rspec'
|
|
# Ensure we override rspec's at_exit
|
|
RSpec::Core::Runner.disable_autorun!
|
|
rescue LoadError => ex
|
|
return ex.to_s
|
|
end
|
|
@hydra_output ||= StringIO.new
|
|
@hydra_output.rewind
|
|
@hydra_output.truncate(0)
|
|
|
|
config = [ file ]
|
|
RSpec.reset
|
|
begin
|
|
result = RSpec::Core::Runner.run(config, @hydra_output, @hydra_output)
|
|
rescue Exception => ex
|
|
return ex.to_s + "\n" + ex.backtrace.join("\n")
|
|
end
|
|
@hydra_output.rewind
|
|
|
|
return (result == 1) ? @hydra_output.read : ""
|
|
end
|
|
|
|
# run all the scenarios in a cucumber feature file
|
|
def run_cucumber_file(file)
|
|
hydra_response = StringIO.new
|
|
|
|
options = @options if @options.is_a?(Array)
|
|
options = @options.split(' ') if @options.is_a?(String)
|
|
|
|
fork_id = fork do
|
|
files = [file]
|
|
dev_null = StringIO.new
|
|
|
|
args = [file, options].flatten.compact
|
|
hydra_response.puts args.inspect
|
|
|
|
results_directory = "#{Dir.pwd}/results/features"
|
|
FileUtils.mkdir_p results_directory
|
|
|
|
require 'cucumber/cli/main'
|
|
require 'hydra/cucumber/formatter'
|
|
require 'hydra/cucumber/partial_html'
|
|
|
|
Cucumber.logger.level = Logger::INFO
|
|
|
|
cuke = Cucumber::Cli::Main.new(args, dev_null, dev_null)
|
|
cuke.configuration.formats << ['Cucumber::Formatter::Hydra', hydra_response]
|
|
|
|
html_output = cuke.configuration.formats.select{|format| format[0] == 'html'}
|
|
if html_output
|
|
cuke.configuration.formats.delete(html_output)
|
|
cuke.configuration.formats << ['Hydra::Formatter::PartialHtml', "#{results_directory}/#{file.split('/').last}.html"]
|
|
end
|
|
|
|
cuke_runtime = Cucumber::Runtime.new(cuke.configuration)
|
|
cuke_runtime.run!
|
|
exit 1 if cuke_runtime.results.failure?
|
|
end
|
|
Process.wait fork_id
|
|
|
|
hydra_response.puts "." if not $?.exitstatus == 0
|
|
hydra_response.rewind
|
|
|
|
hydra_response.read
|
|
end
|
|
|
|
def run_javascript_file(file)
|
|
errors = []
|
|
require 'v8'
|
|
V8::Context.new do |context|
|
|
context.load(File.expand_path(File.join(File.dirname(__FILE__), 'js', 'lint.js')))
|
|
context['input'] = lambda{
|
|
File.read(file)
|
|
}
|
|
context['reportErrors'] = lambda{|js_errors|
|
|
js_errors.each do |e|
|
|
e = V8::To.rb(e)
|
|
errors << "\n\e[1;31mJSLINT: #{file}\e[0m"
|
|
errors << " Error at line #{e['line'].to_i + 1} " +
|
|
"character #{e['character'].to_i + 1}: \e[1;33m#{e['reason']}\e[0m"
|
|
errors << "#{e['evidence']}"
|
|
end
|
|
}
|
|
context.eval %{
|
|
JSLINT(input(), {
|
|
sub: true,
|
|
onevar: true,
|
|
eqeqeq: true,
|
|
plusplus: true,
|
|
bitwise: true,
|
|
regexp: true,
|
|
newcap: true,
|
|
immed: true,
|
|
strict: true,
|
|
rhino: true
|
|
});
|
|
reportErrors(JSLINT.errors);
|
|
}
|
|
end
|
|
|
|
if errors.empty?
|
|
return '.'
|
|
else
|
|
return errors.join("\n")
|
|
end
|
|
end
|
|
|
|
# find all the test unit classes in a given file, so we can run their suites
|
|
def self.find_classes_in_file(f)
|
|
code = ""
|
|
File.open(f) {|buffer| code = buffer.read}
|
|
matches = code.scan(/class\s+([\S]+)/)
|
|
klasses = matches.collect do |c|
|
|
begin
|
|
if c.first.respond_to? :constantize
|
|
c.first.constantize
|
|
else
|
|
eval(c.first)
|
|
end
|
|
rescue NameError
|
|
# means we could not load [c.first], but thats ok, its just not
|
|
# one of the classes we want to test
|
|
nil
|
|
rescue SyntaxError
|
|
# see above
|
|
nil
|
|
end
|
|
end
|
|
return klasses.select{|k| k.respond_to? 'suite'}
|
|
end
|
|
|
|
# Yanked a method from Cucumber
|
|
def tag_excess(features, limits)
|
|
limits.map do |tag_name, tag_limit|
|
|
tag_locations = features.tag_locations(tag_name)
|
|
if tag_limit && (tag_locations.length > tag_limit)
|
|
[tag_name, tag_limit, tag_locations]
|
|
else
|
|
nil
|
|
end
|
|
end.compact
|
|
end
|
|
|
|
def redirect_output file_name
|
|
begin
|
|
$stderr = $stdout = File.open(file_name, 'a')
|
|
rescue
|
|
# it should always redirect output in order to handle unexpected interruption
|
|
# successfully
|
|
$stderr = $stdout = File.open(DEFAULT_LOG_FILE, 'a')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|