about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2009-02-20 20:34:59 -0800
committerEric Wong <normalperson@yhbt.net>2009-02-21 04:23:39 -0800
commit7c396fbd05d01ee5cb1c92def09049db3a3cec9f (patch)
tree03e08f146565035d1604d58c06524132e312b11a
parent38141ffdd3bda01dabfdd8ff8f065c783053c86a (diff)
downloadunicorn-7c396fbd05d01ee5cb1c92def09049db3a3cec9f.tar.gz
This adds a bunch of execution tests that require the "unicorn"
binary to be in PATH as well as rack being directly
"require"-able  ("rubygems" will not be loaded for you).  The
tester is responsible for setting up PATH and RUBYLIB
appropriately.
-rw-r--r--Rakefile5
-rwxr-xr-xbin/unicorn182
-rwxr-xr-xbin/unicorn-hello-world27
-rw-r--r--test/exec/README5
-rw-r--r--test/exec/test_exec.rb430
5 files changed, 601 insertions, 48 deletions
diff --git a/Rakefile b/Rakefile
index f50fd6c..b350f74 100644
--- a/Rakefile
+++ b/Rakefile
@@ -10,13 +10,14 @@ Echoe.new("unicorn") do |p|
   p.url = "http://unicorn.bogomips.org"
   p.rdoc_pattern = ['README', 'LICENSE', 'CONTRIBUTORS', 'CHANGELOG', 'COPYING', 'lib/**/*.rb', 'doc/**/*.rdoc']
   p.ignore_pattern = /^(pkg|site|projects|doc|log)|CVS|\.log/
-  p.extension_pattern = nil
-
   p.need_tar_gz = false
   p.need_tgz = true
 
   p.extension_pattern = ["ext/**/extconf.rb"]
 
+  # Eric hasn't bothered to figure out running exec tests properly
+  # from Rake, but Eric prefers GNU make to Rake for tests anyways...
+  p.test_pattern = [ 'test/unit/test*.rb' ]
 end
 
 #### Ragel builder
diff --git a/bin/unicorn b/bin/unicorn
index 93441ae..f682311 100755
--- a/bin/unicorn
+++ b/bin/unicorn
@@ -1,27 +1,171 @@
 #!/home/ew/bin/ruby
 STDIN.sync = STDOUT.sync = STDERR.sync = true
-usage = "Usage: #{File.basename($0)} <config_file>"
-require 'unicorn'
-exit 0 if ARGV.size == 2 && ARGV[-1] == 'check' # used for reexec_check
-ARGV.size == 1 or abort usage
-case ARGV[0]
-when 'check' then exit
-when '-h' then puts usage
-when '-v' then puts "unicorn v#{Unicorn::Const::UNICORN_VERSION}"
+require 'unicorn' # require this first to populate Unicorn::DEFAULT_START_CTX
+require 'rack'
+require 'optparse'
+
+env = "development"
+daemonize = false
+listeners = []
+options = { :listeners => listeners }
+host = Unicorn::Const::DEFAULT_HOST
+port = Unicorn::Const::DEFAULT_PORT
+
+opts = OptionParser.new("", 24, '  ') do |opts|
+  opts.banner = "Usage: #{File.basename($0)} " \
+                "[ruby options] [unicorn options] [rackup config file]"
+
+  opts.separator ""
+  opts.separator "Ruby options:"
+
+  lineno = 1
+  opts.on("-e", "--eval LINE", "evaluate a LINE of code") do |line|
+    eval line, TOPLEVEL_BINDING, "-e", lineno
+    lineno += 1
+  end
+
+  opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") do
+    $DEBUG = true
+  end
+
+  opts.on("-w", "--warn", "turn warnings on for your script") do
+    $-w = true
+  end
+
+  opts.on("-I", "--include PATH",
+          "specify $LOAD_PATH (may be used more than once)") do |path|
+    $LOAD_PATH.unshift(*path.split(/:/))
+  end
+
+  opts.on("-r", "--require LIBRARY",
+          "require the library, before executing your script") do |library|
+    require library
+  end
+
+  opts.separator ""
+  opts.separator "Unicorn options:"
+
+  # some of these switches exist for rackup command-line compatibility,
+
+  opts.on("-o", "--host HOST", "listen on HOST (default: 0.0.0.0)") do |h|
+    warn "The --host/-o option is not recommended, see --listen/-l"
+    host = h
+  end
+
+  opts.on("-p", "--port PORT", "use PORT (default: 8080)") do |p|
+    warn "The --port/-p option is not recommended, see --listen/-l"
+    port = p.to_i
+  end
+
+  opts.on("-E", "--env ENVIRONMENT",
+          "use ENVIRONMENT for defaults (default: development)") do |e|
+    env = e
+  end
+
+  opts.on("-D", "--daemonize", "run daemonized in the background") do |d|
+    daemonize = d ? true : false
+  end
+
+  opts.on("-P", "--pid FILE", "file to store PID (default: none)") do |f|
+    options[:pid] = File.expand_path(f)
+  end
+
+  # Unicorn-specific stuff
+  opts.on("-l", "--listen <address:port|/path/to/socket>",
+          "listen on address:port or UNIX socket " \
+          "(default: 0.0.0.0:8080)" \
+          "  this may be specified multiple times") do |address|
+    listeners << address
+  end
+
+  opts.on("-c", "--config-file FILE", "Unicorn-specific config file") do |f|
+    options[:config_file] = File.expand_path(f)
+  end
+
+  # I'm avoiding Unicorn-specific config options on the command-line.
+  # IMNSHO, config options on the command-line are redundant given a
+  # config files and make things unnecessarily complicated with multiple
+  # places to look for a config option.
+
+  opts.separator ""
+  opts.separator "Common options:"
+
+  opts.on_tail("-h", "--help", "Show this message") do
+    puts opts
+    exit
+  end
+
+  opts.on_tail("--version", "Show version") do
+    puts "unicorn v#{Unicorn::Const::UNICORN_VERSION}"
+    exit
+  end
+
+  opts.parse! ARGV
+end
+
+require 'pp' if $DEBUG
+
+config = ARGV[0] || "config.ru"
+abort "configuration file #{config} not found" unless File.exist?(config)
+
+if config =~ /\.ru$/
+  cfgfile = File.read(config)
+  # parse embedded command-line options in config.ru comments
+  if cfgfile[/^#\\(.*)/]
+    opts.parse! $1.split(/\s+/)
+  end
+  inner_app = eval "Rack::Builder.new {(#{cfgfile}\n)}.to_app", nil, config
+else
+  require config
+  inner_app = Object.const_get(File.basename(config, '.rb').capitalize)
+end
+
+app = case env
+when "development"
+  Rack::Builder.new do
+    use Rack::CommonLogger, STDERR
+    use Rack::ShowExceptions
+    use Rack::Lint
+    run inner_app
+  end.to_app
+when "deployment"
+  Rack::Builder.new do
+    use Rack::CommonLogger, STDERR
+    run inner_app
+  end.to_app
 else
-  File.readable?(ARGV[0]) && File.file?(ARGV[0]) or abort usage
-  config = eval(File.read(ARGV[0]))
-  config.kind_of?(Hash) or abort "config is not a hash: #{config.class}"
-  app = config.delete(:app) or abort "Missing :app key in config!"
-
-  # only daemonize if we're not inheriting file descriptors from our parent
-  if ENV['UNICORN_DAEMONIZE'] && ! ENV['UNICORN_FD']
-    # don't set umask(0000), chdir("/") or redirect STDOUT/STDERR since
-    # it's more flexible to handle that in the config (which is just Ruby)
+  inner_app
+end
+
+if listeners.empty?
+  listener = "#{host}:#{port}"
+  listeners << listener if listener != Unicorn::Const::DEFAULT_LISTEN
+end
+
+if $DEBUG
+  pp({
+    :unicorn_options => options,
+    :app => app,
+    :inner_app => inner_app,
+    :daemonize => daemonize,
+  })
+end
+
+# only daemonize if we're not inheriting file descriptors from our parent
+if daemonize
+  unless ENV['UNICORN_FD']
     exit if fork
     Process.setsid
     exit if fork
-    STDIN.reopen("/dev/null")
   end
-  Unicorn.run(app, config)
+
+  Dir.chdir("/")
+  File.umask(0000)
+  STDIN.reopen("/dev/null")
+
+  # we can redirect these again in the Unicorn after_fork hook
+  STDOUT.reopen("/dev/null", "a")
+  STDERR.reopen("/dev/null", "a")
 end
+
+Unicorn.run(app, options)
diff --git a/bin/unicorn-hello-world b/bin/unicorn-hello-world
deleted file mode 100755
index e368344..0000000
--- a/bin/unicorn-hello-world
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/home/ew/bin/ruby
-# Simple "Hello World" application for Unicorn
-
-# Exec ourselves with unicorn.  A shebang (e.g. "#!/usr/bin/unicorn")
-# won't work since unicorn itself is a Ruby script with a shebang, but
-# this does:
-exec('unicorn', $0) if $0 == __FILE__
-
-# Rack-compatible "Hello World" application
-class HelloWorld
-  MSG = "Hello world!\n"
-
-  def call(env)
-    [ 200,
-       { "Content-Type" => "text/plain",
-         "Content-Length" => MSG.size},
-     [ MSG ]
-    ]
-  end
-end
-
-# make sure this hash is the last statement, as this is eval-ed by unicorn
-{
-  # :listeners => %w(0.0.0.0:8080 127.0.0.1:7701 /tmp/test.sock),
-  # :hot_config_file => "/tmp/hot_config",
-  :app => HelloWorld.new,
-}
diff --git a/test/exec/README b/test/exec/README
new file mode 100644
index 0000000..a1341f5
--- /dev/null
+++ b/test/exec/README
@@ -0,0 +1,5 @@
+These tests require the "unicorn" executable script to be installed in
+PATH and rack being directly "require"-able ("rubygems" will not be
+loaded for you).  The tester is responsible for setting up RUBYLIB and
+PATH environment variables (or running tests via GNU Make instead of
+Rake).
diff --git a/test/exec/test_exec.rb b/test/exec/test_exec.rb
new file mode 100644
index 0000000..4e460e2
--- /dev/null
+++ b/test/exec/test_exec.rb
@@ -0,0 +1,430 @@
+# Copyright (c) 2009 Eric Wong
+STDIN.sync = STDOUT.sync = STDERR.sync = true
+require 'test/test_helper'
+require 'pathname'
+require 'tempfile'
+require 'fileutils'
+
+do_test = true
+
+$unicorn_bin = ENV['UNICORN_TEST_BIN'] || "unicorn"
+pid = fork { redirect_test_io { exec($unicorn_bin, '-v'); exit!(1) } }
+Process.waitpid(pid)
+unless $?.success?
+  STDERR.puts "#{$unicorn_bin} not found in PATH=#{ENV['PATH']}, "\
+              "skipping this test"
+  do_test = false
+end
+
+begin
+  require 'rack'
+rescue LoadError
+  STDERR.puts "Unable to load Rack, skipping this test"
+  do_test = false
+end
+
+class ExecTest < Test::Unit::TestCase
+  trap('QUIT', 'IGNORE')
+
+  HI = <<-EOS
+use Rack::ContentLength
+run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, "HI\\n" ] }
+  EOS
+
+  HELLO = <<-EOS
+class Hello
+  def call(env)
+    [ 200, { 'Content-Type' => 'text/plain' }, "HI\\n" ]
+  end
+end
+  EOS
+
+  COMMON_TMP = Tempfile.new('unicorn_tmp') unless defined?(COMMON_TMP)
+
+  HEAVY_CFG = <<-EOS
+require 'fcntl'
+worker_processes 4
+timeout 30
+backlog 1
+logger Logger.new('#{COMMON_TMP.path}')
+before_fork do |server, worker_nr|
+  server.logger.info "before_fork: worker=\#{worker_nr}"
+end
+after_fork do |server, worker_nr|
+  trap('USR1') do # log rotation
+    server.logger.info "after_fork: worker=\#{worker_nr} rotating logs..."
+    ObjectSpace.each_object(File) do |fp|
+      next if fp.closed? || ! fp.sync
+      next unless (fp.fcntl(Fcntl::F_GETFL) & File::APPEND) == File::APPEND
+      begin
+        fp.stat.ino == File.stat(fp.path).ino
+      rescue Errno::ENOENT
+      end
+      fp.reopen(fp.path, "a")
+      fp.sync = true
+    end
+    server.logger.info "after_fork: worker=\#{worker_nr} done rotating logs"
+  end # trap('USR1')
+end # after_fork
+  EOS
+
+  def setup
+    @pwd = Dir.pwd
+    @tmpfile = Tempfile.new('unicorn_exec_test')
+    @tmpdir = @tmpfile.path
+    @tmpfile.close!
+    Dir.mkdir(@tmpdir)
+    Dir.chdir(@tmpdir)
+    @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
+    @port = unused_port(@addr)
+    @sockets = []
+  end
+
+  def teardown
+    Dir.chdir(@pwd)
+    FileUtils.rmtree(@tmpdir)
+    @sockets.each { |path| File.unlink(path) rescue nil }
+    loop do
+      Process.kill('-QUIT', 0)
+      begin
+        Process.waitpid(-1, Process::WNOHANG) or break
+      rescue Errno::ECHILD
+        break
+      end
+    end
+  end
+
+  def test_basic
+    File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
+    pid = fork do
+      redirect_test_io { exec($unicorn_bin, "-l", "#{@addr}:#{@port}") }
+    end
+    results = retry_hit(["http://#{@addr}:#{@port}/"])
+    assert_equal String, results[0].class
+    assert_shutdown(pid)
+  end
+
+  def test_unicorn_config_listeners_overrides_cli
+    port2 = unused_port(@addr)
+    File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
+    # listeners = [ ... ]  => should _override_ command-line options
+    ucfg = Tempfile.new('unicorn_test_config')
+    ucfg.syswrite("listeners %w(#{@addr}:#{@port})\n")
+    pid = fork do
+      redirect_test_io do
+        exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
+      end
+    end
+    results = retry_hit(["http://#{@addr}:#{@port}/"])
+    assert_raises(Errno::ECONNREFUSED) { TCPSocket.new(@addr, port2) }
+    assert_equal String, results[0].class
+    assert_shutdown(pid)
+  end
+
+  def test_unicorn_config_listen_augments_cli
+    port2 = unused_port(@addr)
+    File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
+    ucfg = Tempfile.new('unicorn_test_config')
+    ucfg.syswrite("listen '#{@addr}:#{@port}'\n")
+    pid = fork do
+      redirect_test_io do
+        exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
+      end
+    end
+    uris = [@port, port2].map { |i| "http://#{@addr}:#{i}/" }
+    results = retry_hit(uris)
+    assert_equal results.size, uris.size
+    assert_equal String, results[0].class
+    assert_equal String, results[1].class
+    assert_shutdown(pid)
+  end
+
+  def test_weird_config_settings
+    File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
+    ucfg = Tempfile.new('unicorn_test_config')
+    ucfg.syswrite(HEAVY_CFG)
+    pid = fork do
+      redirect_test_io do
+        exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{@port}")
+      end
+    end
+
+    results = retry_hit(["http://#{@addr}:#{@port}/"])
+    assert_equal String, results[0].class
+    wait_master_ready(COMMON_TMP.path)
+    bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/)
+    assert_equal 4, bf.size
+    rotate = Tempfile.new('unicorn_rotate')
+    assert_nothing_raised do
+      File.rename(COMMON_TMP.path, rotate.path)
+      Process.kill('USR1', pid)
+    end
+    wait_for_file(COMMON_TMP.path)
+    assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists"
+    # USR1 should've been passed to all workers
+    tries = 100
+    log = File.readlines(rotate.path)
+    while (tries -= 1) > 0 && log.grep(/rotating logs\.\.\./).size < 4
+      sleep 0.1
+      log = File.readlines(rotate.path)
+    end
+    assert_equal 4, log.grep(/rotating logs\.\.\./).size
+    assert_equal 0, log.grep(/done rotating logs/).size
+
+    tries = 100
+    log = File.readlines(COMMON_TMP.path)
+    while (tries -= 1) > 0 && log.grep(/done rotating logs/).size < 4
+      sleep 0.1
+      log = File.readlines(COMMON_TMP.path)
+    end
+    assert_equal 4, log.grep(/done rotating logs/).size
+    assert_equal 0, log.grep(/rotating logs\.\.\./).size
+    assert_nothing_raised { Process.kill('QUIT', pid) }
+    status = nil
+    assert_nothing_raised { pid, status = Process.waitpid2(pid) }
+    assert status.success?, "exited successfully"
+  end
+
+  def test_read_embedded_cli_switches
+    File.open("config.ru", "wb") do |fp|
+      fp.syswrite("#\\ -p #{@port} -o #{@addr}\n")
+      fp.syswrite(HI)
+    end
+    pid = fork { redirect_test_io { exec($unicorn_bin) } }
+    results = retry_hit(["http://#{@addr}:#{@port}/"])
+    assert_equal String, results[0].class
+    assert_shutdown(pid)
+  end
+
+  def test_config_ru_alt_path
+    config_path = "#{@tmpdir}/foo.ru"
+    File.open(config_path, "wb") { |fp| fp.syswrite(HI) }
+    pid = fork do
+      redirect_test_io do
+        Dir.chdir("/")
+        exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
+      end
+    end
+    results = retry_hit(["http://#{@addr}:#{@port}/"])
+    assert_equal String, results[0].class
+    assert_shutdown(pid)
+  end
+
+  def test_load_module
+    libdir = "#{@tmpdir}/lib"
+    FileUtils.mkpath([ libdir ])
+    config_path = "#{libdir}/hello.rb"
+    File.open(config_path, "wb") { |fp| fp.syswrite(HELLO) }
+    pid = fork do
+      redirect_test_io do
+        Dir.chdir("/")
+        exec($unicorn_bin, "-l#{@addr}:#{@port}", config_path)
+      end
+    end
+    results = retry_hit(["http://#{@addr}:#{@port}/"])
+    assert_equal String, results[0].class
+    assert_shutdown(pid)
+  end
+
+  def test_reexec
+    File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
+    pid_file = "#{@tmpdir}/test.pid"
+    pid = fork do
+      redirect_test_io do
+        exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}")
+      end
+    end
+    reexec_basic_test(pid, pid_file)
+  end
+
+  def test_reexec_alt_config
+    config_file = "#{@tmpdir}/foo.ru"
+    File.open(config_file, "wb") { |fp| fp.syswrite(HI) }
+    pid_file = "#{@tmpdir}/test.pid"
+    pid = fork do
+      redirect_test_io do
+        exec($unicorn_bin, "-l#{@addr}:#{@port}", "-P#{pid_file}", config_file)
+      end
+    end
+    reexec_basic_test(pid, pid_file)
+  end
+
+  def test_unicorn_config_file
+    pid_file = "#{@tmpdir}/test.pid"
+    sock = Tempfile.new('unicorn_test_sock')
+    sock_path = sock.path
+    sock.close!
+    @sockets << sock_path
+
+    log = Tempfile.new('unicorn_test_log')
+    ucfg = Tempfile.new('unicorn_test_config')
+    ucfg.syswrite("listen \"#{sock_path}\"\n")
+    ucfg.syswrite("pid \"#{pid_file}\"\n")
+    ucfg.syswrite("logger Logger.new('#{log.path}')\n")
+    ucfg.close
+
+    File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
+    pid = fork do
+      redirect_test_io do
+        exec($unicorn_bin, "-l#{@addr}:#{@port}",
+             "-P#{pid_file}", "-c#{ucfg.path}")
+      end
+    end
+    results = retry_hit(["http://#{@addr}:#{@port}/"])
+    assert_equal String, results[0].class
+    wait_master_ready(log.path)
+    assert File.exist?(pid_file), "pid_file created"
+    assert_equal pid, File.read(pid_file).to_i
+    assert File.socket?(sock_path), "socket created"
+    assert_nothing_raised do
+      sock = UNIXSocket.new(sock_path)
+      sock.syswrite("GET / HTTP/1.0\r\n\r\n")
+      results = sock.sysread(4096)
+    end
+    assert_equal String, results.class
+
+    # try reloading the config
+    sock = Tempfile.new('unicorn_test_sock')
+    new_sock_path = sock.path
+    @sockets << new_sock_path
+    sock.close!
+    new_log = Tempfile.new('unicorn_test_log')
+    new_log.sync = true
+    assert_equal 0, new_log.size
+
+    assert_nothing_raised do
+      ucfg = File.open(ucfg.path, "wb")
+      ucfg.syswrite("listen \"#{new_sock_path}\"\n")
+      ucfg.syswrite("pid \"#{pid_file}\"\n")
+      ucfg.syswrite("logger Logger.new('#{new_log.path}')\n")
+      ucfg.close
+      Process.kill('HUP', pid)
+    end
+
+    wait_for_file(new_sock_path)
+    assert File.socket?(new_sock_path), "socket exists"
+    @sockets.each do |path|
+      assert_nothing_raised do
+        sock = UNIXSocket.new(path)
+        sock.syswrite("GET / HTTP/1.0\r\n\r\n")
+        results = sock.sysread(4096)
+      end
+      assert_equal String, results.class
+    end
+
+    assert_not_equal 0, new_log.size
+    reexec_usr2_quit_test(pid, pid_file)
+  end
+
+  def test_daemonize_reexec
+    pid_file = "#{@tmpdir}/test.pid"
+    log = Tempfile.new('unicorn_test_log')
+    ucfg = Tempfile.new('unicorn_test_config')
+    ucfg.syswrite("pid \"#{pid_file}\"\n")
+    ucfg.syswrite("logger Logger.new('#{log.path}')\n")
+    ucfg.close
+
+    File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
+    pid = fork do
+      redirect_test_io do
+        exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
+      end
+    end
+    results = retry_hit(["http://#{@addr}:#{@port}/"])
+    assert_equal String, results[0].class
+    wait_for_file(pid_file)
+    new_pid = File.read(pid_file).to_i
+    assert_not_equal pid, new_pid
+    pid, status = Process.waitpid2(pid)
+    assert status.success?, "original process exited successfully"
+    assert_nothing_raised { Process.kill(0, new_pid) }
+    reexec_usr2_quit_test(new_pid, pid_file)
+  end
+
+  private
+
+    # sometimes the server may not come up right away
+    def retry_hit(uris = [])
+      tries = 100
+      begin
+        hit(uris)
+      rescue Errno::ECONNREFUSED => err
+        if (tries -= 1) > 0
+          sleep 0.1
+          retry
+        end
+        raise err
+      end
+    end
+
+    def assert_shutdown(pid)
+      wait_master_ready("#{@tmpdir}/test_stderr.#{pid}.log")
+      assert_nothing_raised { Process.kill('QUIT', pid) }
+      status = nil
+      assert_nothing_raised { pid, status = Process.waitpid2(pid) }
+      assert status.success?, "exited successfully"
+    end
+
+    def wait_master_ready(master_log)
+      tries = 100
+      while (tries -= 1) > 0
+        begin
+          File.readlines(master_log).grep(/master process ready/)[0] and return
+        rescue Errno::ENOENT
+        end
+        sleep 0.1
+      end
+      raise "master process never became ready"
+    end
+
+    def reexec_usr2_quit_test(pid, pid_file)
+      assert File.exist?(pid_file), "pid file OK"
+      assert ! File.exist?("#{pid_file}.oldbin"), "oldbin pid file"
+      assert_nothing_raised { Process.kill('USR2', pid) }
+      assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
+      wait_for_file("#{pid_file}.oldbin")
+      wait_for_file(pid_file)
+
+      # kill old master process
+      assert_not_equal pid, File.read(pid_file).to_i
+      assert_equal pid, File.read("#{pid_file}.oldbin").to_i
+      assert_nothing_raised { Process.kill('QUIT', pid) }
+      assert_not_equal pid, File.read(pid_file).to_i
+      assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
+      wait_for_file(pid_file)
+      assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
+      assert_nothing_raised { Process.kill('QUIT', File.read(pid_file).to_i) }
+    end
+
+    def reexec_basic_test(pid, pid_file)
+      results = retry_hit(["http://#{@addr}:#{@port}/"])
+      assert_equal String, results[0].class
+      assert_nothing_raised { Process.kill(0, pid) }
+      master_log = "#{@tmpdir}/test_stderr.#{pid}.log"
+      wait_master_ready(master_log)
+      File.truncate(master_log, 0)
+      nr = 50
+      kill_point = 2
+      assert_nothing_raised do
+        nr.times do |i|
+          hit(["http://#{@addr}:#{@port}/#{i}"])
+          i == kill_point and Process.kill('HUP', pid)
+        end
+      end
+      wait_master_ready(master_log)
+      assert File.exist?(pid_file), "pid=#{pid_file} exists"
+      new_pid = File.read(pid_file).to_i
+      assert_not_equal pid, new_pid
+      assert_nothing_raised { Process.kill(0, new_pid) }
+      assert_nothing_raised { Process.kill('QUIT', new_pid) }
+    end
+
+    def wait_for_file(path)
+      tries = 1000
+      while (tries -= 1) > 0 && ! File.exist?(path)
+        sleep 0.1
+      end
+      assert File.exist?(path), "path=#{path} exists"
+    end
+
+end if do_test