class Unobtainium::Drivers::Phantom

Driver implementation using the selenium-webdriver gem to connect to PhantomJS.

Constants

CONNECT_TIMEOUT

Timeout for waiting for connecting to PhantomJS server, in seconds

LABELS

Recognized labels for matching the driver

PORT_RANGES

Port scanning ranges (can also be arrays or single port numbers.

Public Class Methods

create(_, options) click to toggle source

Create and return a driver instance

# File lib/unobtainium/drivers/phantom.rb, line 125
def create(_, options)
  # :nocov:

  # Extract PhantomJS options
  host = options['phantomjs.host']
  port = options['phantomjs.port'] || options['phantomjs.generated_port']
  opts = options.dup
  opts.delete('phantomjs')

  # Start PhantomJS server, if it isn't already running
  conn_str = "#{host}:#{port}"
  runner = ::Unobtainium::Runtime.instance.store_with_if(conn_str) do
    ::Unobtainium::Support::Runner.new(conn_str,
                                       Phantomjs.path,
                                       "--webdriver=#{conn_str}")
  end
  runner.start

  # Wait for the server to open a port.
  timeout = CONNECT_TIMEOUT
  while timeout > 0 and not port_open?(host, port)
    sleep 1
    timeout -= 1
  end
  if timeout <= 0
    runner.kill
    out = runner.stdout.read
    err = runner.stderr.read
    runner.reset

    raise "Timeout waiting to connect to PhantomJS!\n"\
      "STDOUT: #{out}\n"\
      "STDERR: #{err}"
  end

  # Run Selenium against server
  driver = ::Selenium::WebDriver.for(:remote, opts)
  return driver

  # :nocov:
end
ensure_preconditions(_, _) click to toggle source

Ensure that the driver's preconditions are fulfilled.

Calls superclass method
# File lib/unobtainium/drivers/phantom.rb, line 53
def ensure_preconditions(_, _)
  super
  begin
    require 'phantomjs'
  rescue LoadError => err
    raise LoadError, "#{err.message}: you need to add "\
          "'phantomjs' to your Gemfile to use this driver!",
          err.backtrace
  end
end
resolve_options(label, options) click to toggle source

Mostly provides webdriver-specific options for PhantomJS

Calls superclass method
# File lib/unobtainium/drivers/phantom.rb, line 66
def resolve_options(label, options)
  label, options = super

  options = ::Collapsium::UberHash.new(options)

  # If a URL is already provided, we should respect this.
  merge_url(options)

  # Provide defaults for webdriver host and port.
  merge_defaults(options)

  # At this point, the :phantomjs field is canonical in that it will
  # be used to generate a :port and :url if necessary. That means we
  # can use it to create a stable ID, too.
  # This also implies that the :url field is pointless and should not
  # be part of the ID; it will be generated again later on.
  options.delete(:url)

  # We need to figure out what we have to do based on detecting whether
  # a port or some other option changed (or nothing did!)
  fix_ports(label, options)

  # We find a free port here, so there's a possibility it'll get used
  # before we run the server in #create. However, for the purpose of
  # resolving options that's necessary. So we'll just live with this
  # until it becomes a problem.
  if options['phantomjs.generated_port'].nil?
    if options['phantomjs.port'].nil?
      ports = scan(options['phantomjs.host'], *PORT_RANGES,
                   for: :available, amount: :first)
      if ports.empty?
        raise "Could not find an available port for the PhantomJS server!"
      end
      options['phantomjs.generated_port'] = ports[0]
    else
      options['phantomjs.generated_port'] = options['phantomjs.port']
    end
  end

  # Now we can't just use new_id because we might have found a new
  # port in the meantime. We'll have to generate yet another ID, and
  # use that.
  # Now before calculating this new ID, we'll run the options through
  # the super method again. This is to ensure that all keys have the
  # expected class *before* we perform this calculation.
  new_id = identifier('driver', label, options)
  options['unobtainium_instance_id'] = new_id

  # Now we can generate the :url field for Selenium's benefit; it's
  # just a copy of the canonical options.
  options[:url] = "#{options['phantomjs.scheme']}://"\
      "#{options['phantomjs.host']}:"\
      "#{options['phantomjs.generated_port']}"

  return label, options
end

Private Class Methods

fix_ports(label, options) click to toggle source
# File lib/unobtainium/drivers/phantom.rb, line 209
def fix_ports(label, options)
  # Let's keep the old ID around and generate a new one, largely as
  # a checksum of the options.
  old_id = options.delete('unobtainium_instance_id')
  new_id = identifier('driver', label, options)

  # If the IDs don't match, it means some options changed. This may be
  # the port or another option:
  # #1 If IDs are the same and port is the same as generated port, we
  #    need not do anything.
  # #2 If IDs are the same and the ports differ, we have reached an
  #    undefined state. This should be impossible.
  # #3 If IDs differ and the ports are the same, some other option was
  #    changed. We need to generate a new port and new ID (and warn about
  #    this).
  # #4 If IDs differ and ports differ, a new port was set. We need to
  #    proceed with the new port and generate a new ID.
  port = options['phantomjs.port']
  generated_port = options['phantomjs.generated_port']

  if old_id == new_id and port == generated_port
    # #1 above, nothing to do.
  elsif old_id == new_id and port != generated_port
    # :nocov:
    # #2 above, raise hell
    if not port.nil?
      raise "This can't happen; the instance ID (checksum) is the same, "\
        "but the input differed: #{port} <-> #{generated_port}"
    end
    # :nocov:
  elsif old_id != new_id and port == generated_port
    # #3 above; nuke the ports and warn.
    if not port.nil? and not generated_port.nil?
      warn "Rejecting port #{port} and generating new port because "\
        "options changed."
    end
    options['phantomjs.port'] = nil
    options['phantomjs.generated_port'] = nil
  elsif old_id != new_id and port != generated_port
    # #4 above
    options['phantomjs.generated_port'] = nil
  else
    # :nocov:
    # Unreachable
    raise "This can't happen; we have four cases and handle each of "\
      "them. This line is unreachable. Please check the logic."
    # :nocov:
  end
end
merge_defaults(options) click to toggle source
# File lib/unobtainium/drivers/phantom.rb, line 197
def merge_defaults(options)
  defaults = {
    phantomjs: {
      scheme: 'http',
      host: 'localhost',
      port: nil,
      generated_port: nil,
    },
  }
  options.recursive_merge!(defaults, false)
end
merge_url(options) click to toggle source
# File lib/unobtainium/drivers/phantom.rb, line 169
def merge_url(options)
  if not options[:url]
    return
  end

  require 'uri'
  parsed = URI.parse(options[:url])
  parsed_port = parsed.port.to_i
  from_parsed = {
    phantomjs: {
      scheme: parsed.scheme,
      host: parsed.host,
      port: nil,
    },
  }

  # Very special case: if the parsed port matches the generated port,
  # and the port is nil, we want to keep it that way for deduplication
  # purposes. See `#fix_ports` for how this interacts.
  port = options['phantomjs.port']
  generated_port = options['phantomjs.generated_port']
  if not (parsed_port == generated_port and port.nil?)
    from_parsed[:phantomjs][:port] = parsed_port
  end

  options.recursive_merge!(from_parsed, false)
end