class Nitra::Workers::Worker

Attributes

channel[R]
configuration[R]
io[R]
runner_id[R]
worker_number[R]

Public Class Methods

framework_name() click to toggle source

Return the framework name of this worker

# File lib/nitra/worker.rb, line 22
def framework_name
  self.name.split("::").last.downcase
end
inherited(klass) click to toggle source
# File lib/nitra/worker.rb, line 11
def inherited(klass)
  @@worker_classes[klass.framework_name] = klass
end
new(runner_id, worker_number, configuration) click to toggle source
# File lib/nitra/worker.rb, line 30
def initialize(runner_id, worker_number, configuration)
  @runner_id = runner_id
  @worker_number = worker_number
  @configuration = configuration
  @forked_worker_pid = nil

  ENV["TEST_ENV_NUMBER"] = worker_number.to_s

  # Frameworks don't like it when you change the IO between invocations.
  # So we make one object and flush it after every invocation.
  @io = StringIO.new
end
worker_classes() click to toggle source
# File lib/nitra/worker.rb, line 15
def worker_classes
  @@worker_classes
end

Public Instance Methods

fork_and_run() click to toggle source
# File lib/nitra/worker.rb, line 44
def fork_and_run
  client, server = Nitra::Channel.pipe

  pid = fork do
    # This is important. We don't want anything bubbling up to the master that we didn't send there.
    # We reopen later to get the output from the framework run.
    $stdout.reopen('/dev/null', 'a')
    $stderr.reopen('/dev/null', 'a')

    trap("USR1") { interrupt_forked_worker_and_exit }

    server.close
    @channel = client
    begin
      run
    rescue => e
      channel.write("command" => "error", "process" => "init framework", "text" => e.message, "worker_number" => worker_number)
    end
  end

  client.close

  [pid, server]
end

Protected Instance Methods

clean_up() click to toggle source
# File lib/nitra/worker.rb, line 82
def clean_up
  raise 'Subclasses must impliment this method.'
end
connect_to_database() click to toggle source
# File lib/nitra/worker.rb, line 139
def connect_to_database
  if defined?(Rails)
    Nitra::RailsTooling.connect_to_database
    debug("Connected to database #{ActiveRecord::Base.connection.current_database}")
  end
end
debug(*text) click to toggle source

Sends debug data up to the runner.

# File lib/nitra/worker.rb, line 204
def debug(*text)
  if configuration.debug
    channel.write("command" => "debug", "text" => "worker #{runner_id}.#{worker_number}: #{text.join}", "worker_number" => worker_number)
  end
end
interrupt_forked_worker_and_exit() click to toggle source

Interrupts the forked worker cleanly and exits

# File lib/nitra/worker.rb, line 195
def interrupt_forked_worker_and_exit
  Process.kill('USR1', @forked_worker_pid) if @forked_worker_pid
  Process.waitall
  exit
end
load_environment() click to toggle source
# File lib/nitra/worker.rb, line 70
def load_environment
  raise 'Subclasses must impliment this method.'
end
minimal_file() click to toggle source
# File lib/nitra/worker.rb, line 74
def minimal_file
  raise 'Subclasses must impliment this method.'
end
preload_framework() click to toggle source
# File lib/nitra/worker.rb, line 118
def preload_framework
  debug "running empty spec/feature to make framework run its initialisation"
  file = Tempfile.new("nitra")
  begin
    load_environment
    file.write(minimal_file)
    file.close

    output = Nitra::Utils.capture_output do
      run_file(file.path, true)
    end

    channel.write("command" => "stdout", "process" => "init framework", "text" => output, "worker_number" => worker_number) unless output.empty?
  ensure
    file.close unless file.closed?
    file.unlink
    io.string = ""
  end
  clean_up
end
process_file(filename) click to toggle source

Process the file, forking before hand.

There’s two sets of data we’re interested in, the output from the test framework, and any other output. 1) We capture the framework’s output in the @io object and send that up to the runner in a results message. This happens in the run_x_file methods. 2) Anything else we capture off the stdout/stderr using the pipe and fire off in the stdout message.

# File lib/nitra/worker.rb, line 158
def process_file(filename)
  debug "Starting to process #{filename}"
  start_time = Time.now

  rd, wr = IO.pipe
  @forked_worker_pid = fork do
    trap('USR1') { exit! }  # at_exit hooks will be run in the parent.
    $stdout.reopen(wr)
    $stderr.reopen(wr)
    rd.close
    $0 = filename
    run_file(filename)
    wr.close
    exit!  # at_exit hooks will be run in the parent.
  end
  wr.close
  output = ""
  loop do
    IO.select([rd])
    text = rd.read
    break if text.nil? || text.length.zero?
    output.concat text
  end
  rd.close
  Process.wait(@forked_worker_pid) if @forked_worker_pid

  @forked_worker_pid = nil

  end_time = Time.now
  channel.write("command" => "stdout", "process" => "test framework", "filename" => filename, "text" => output, "worker_number" => worker_number) unless output.empty?
  debug "#{filename} processed in #{'%0.2f' % (end_time - start_time)}s"
end
reset_cache() click to toggle source
# File lib/nitra/worker.rb, line 146
def reset_cache
  Nitra::RailsTooling.reset_cache if defined?(Rails)
end
run() click to toggle source
# File lib/nitra/worker.rb, line 86
def run
  trap("SIGTERM") do
    channel.write("command" => "error", "process" => "trap", "text" => 'Received SIGTERM', "worker_number" => worker_number)
    Process.kill("SIGKILL", Process.pid)
  end
  trap("SIGINT") do
    channel.write("command" => "error", "process" => "trap", "text" => 'Received SIGINT', "worker_number" => worker_number)
    Process.kill("SIGKILL", Process.pid) 
  end

  debug "Started, using TEST_ENV_NUMBER #{ENV['TEST_ENV_NUMBER']}"
  connect_to_database
  reset_cache

  preload_framework

  # Loop until our runner passes us a message from the master to tells us we're finished.
  loop do
    debug "Announcing availability"
    channel.write("command" => "ready", "framework" => self.class.framework_name, "worker_number" => worker_number)
    debug "Waiting for next job"
    data = channel.read
    if data.nil? || data["command"] == "close"
      debug "Channel closed, exiting"
      exit
    elsif data['command'] == "process"
      filename = data["filename"].chomp
      process_file(filename)
    end
  end
end
run_file(filename, preload = false) click to toggle source
# File lib/nitra/worker.rb, line 78
def run_file(filename, preload = false)
  raise 'Subclasses must impliment this method.'
end