lib/kookaburra/rack_app_server.rb
require 'capybara'
require 'thwait'
require 'find_a_port'
# Handles Starting/Stopping Rack Server for Tests
#
# {RackAppServer} is basically a wrapper around {Capybara::Server} that
# makes it a bit easier to use Kookaburra with a Rack application (such
# as Rails or Sinatra) when you want to run your tests locally against a
# server that is only running for the duration of the tests. You simply
# tell it how to get ahold of your Rack application (see {#initialize})
# and then call {#boot} before your tests run and {#shutdown} after your
# tests run.
#
# @example using RSpec
# # put this in something like `spec_helper.rb`
# app_server = Kookaburra::RackAppServer.new do
# # unless you are in JRuby, this stuff all runs in a new fork later
# # on
# ENV['RAILS_ENV'] = 'test'
# require File.expand_path(File.join('..', '..', 'config', 'environment'), __FILE__)
# MyAppName::Application
# end
# RSpec.configure do
# c.before(:all) do
# app_server.boot
# end
# c.after(:all) do
# app_server.shutdown
# end
# end
class Kookaburra::RackAppServer
attr_reader :port
# Sets up a new app server
#
# @param startup_timeout [Integer] The maximum number of seconds to
# wait for the app server to respond
# @yieldreturn [#call] block must return a valid Rack application
def initialize(startup_timeout=10, &rack_app_initializer)
self.startup_timeout = startup_timeout
self.rack_app_initializer = rack_app_initializer
self.port = FindAPort.available_port
end
# Start the application server
#
# This will launch the server on a (detected to be) available port. It
# will then monitor that port and only return once the app server is
# responding (or after the timeout period specified on {#initialize}).
def boot
fork_app_server
wait_for_app_to_respond
end
def shutdown
Process.kill(9, rack_server_pid)
Process.wait
end
private
attr_accessor :rack_app_initializer, :rack_server_pid, :startup_timeout
attr_writer :port
def fork_app_server
self.rack_server_pid = fork do
start_server
end
end
def start_server
app = rack_app_initializer.call
Capybara.server_port = port
Capybara::Server.new(app).boot
# This ensures that this forked process keeps running, because the
# actual server is started in a thread by Capybara.
ThreadsWait.all_waits(Thread.list)
end
def wait_for_app_to_respond
begin
Timeout.timeout(startup_timeout) do
next until running?
end
rescue Timeout::Error
raise "Application does not seem to be responding on port #{port}."
end
end
def running?
case (Net::HTTP.start('localhost', port) { |http|
attempts_remaining = 3
begin
http.get('/__identify__')
rescue Net::HTTPBadResponse => e
if attempts_remaining > 0
attempts_remaining -= 1
retry
else
raise e
end
end
})
when Net::HTTPSuccess, Net::HTTPRedirection
true
else
false
end
rescue Errno::ECONNREFUSED, Errno::EBADF, Errno::ETIMEDOUT
false
end
end