class Hydra::Runner

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.

Constants

DEFAULT_LOG_FILE

Public Class Methods

new(opts = {}) click to toggle source

Boot up a runner. It takes an IO object (generally a pipe from its parent) to send it messages on which files to execute.

# File lib/hydra/runner.rb, line 21
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) { "" }
  @directory = get_directory

  $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

Private Class Methods

find_classes_in_file(f) click to toggle source

find all the test unit classes in a given file, so we can run their suites

# File lib/hydra/runner.rb, line 262
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

Public Instance Methods

format_exception(ex) click to toggle source
# File lib/hydra/runner.rb, line 90
def format_exception(ex)
  "#{ex.class.name}: #{ex.message}\n    #{ex.backtrace.join("\n    ")}"
end
reg_trap_sighup() click to toggle source
# File lib/hydra/runner.rb, line 44
def reg_trap_sighup
  for sign in [:SIGHUP, :INT]
    trap sign do
      stop
    end
  end
  @runner_began = true
end
run_file(file) click to toggle source

Run a test file and report the results

# File lib/hydra/runner.rb, line 59
def run_file(file)
  trace "Running file: #{file}"

  output = ""
  if file =~ /_spec.rb$/
    output = run_rspec_file(file)
  elsif file =~ /.feature$/
    output = run_cucumber_file(file)
  elsif file =~ /.js$/ or file =~ /.json$/
    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
runner_begin() click to toggle source
# File lib/hydra/runner.rb, line 53
def runner_begin
  trace "Firing runner_begin event"
  @event_listeners.each {|l| l.runner_begin( self ) }
end
runner_end() click to toggle source
# File lib/hydra/runner.rb, line 85
def runner_end
  trace "Ending runner #{self.inspect}"
  @event_listeners.each {|l| l.runner_end( self ) }
end
stop() click to toggle source

Stop running

# File lib/hydra/runner.rb, line 80
def stop
  runner_end if @runner_began
  @runner_began = @running = false
end

Private Instance Methods

format_ex_in_file(file, ex) click to toggle source
# File lib/hydra/runner.rb, line 117
def format_ex_in_file(file, ex)
  "Error in #{file}:\n  #{format_exception(ex)}"
end
get_directory() click to toggle source
# File lib/hydra/runner.rb, line 307
def get_directory
  RUBY_VERSION < "1.9" ? "" : Dir.pwd + "/"
end
process_messages() click to toggle source

The runner will continually read messages and handle them.

# File lib/hydra/runner.rb, line 97
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
redirect_output(file_name) click to toggle source
# File lib/hydra/runner.rb, line 297
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
run_cucumber_file(file) click to toggle source

run all the scenarios in a cucumber feature file

# File lib/hydra/runner.rb, line 177
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
run_javascript_file(file) click to toggle source
# File lib/hydra/runner.rb, line 220
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 %Q{
      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
run_rspec_file(file) click to toggle source

run all the Specs in an RSpec file (NOT IMPLEMENTED)

# File lib/hydra/runner.rb, line 149
def run_rspec_file(file)
  # pull in rspec
  begin
    require 'rspec'
    require 'hydra/spec/hydra_formatter'
    # Ensure we override rspec's at_exit
    RSpec::Core::Runner.disable_autorun!
  rescue LoadError => ex
    return ex.to_s
  end
  hydra_output = StringIO.new

  config = [
    '-f', 'RSpec::Core::Formatters::HydraFormatter',
    file
  ]

  RSpec.instance_variable_set(:@world, nil)
  RSpec::Core::Runner.run(config, hydra_output, hydra_output)

  hydra_output.rewind
  output = hydra_output.read.chomp
  output = "" if output.gsub("\n","") =~ /^\.*$/

  return output
end
run_test_unit_file(file) click to toggle source

Run all the Test::Unit Suites in a ruby file

# File lib/hydra/runner.rb, line 122
def run_test_unit_file(file)
  begin
    require @directory + 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
tag_excess(features, limits) click to toggle source

Yanked a method from Cucumber

# File lib/hydra/runner.rb, line 286
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