about summary refs log tree commit homepage
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/exec/test_exec.rb370
-rw-r--r--test/rails/app-1.2.3/.gitignore2
-rw-r--r--test/rails/app-1.2.3/Rakefile7
-rw-r--r--test/rails/app-1.2.3/app/controllers/application.rb4
-rw-r--r--test/rails/app-1.2.3/app/controllers/foo_controller.rb34
-rw-r--r--test/rails/app-1.2.3/app/helpers/application_helper.rb2
-rw-r--r--test/rails/app-1.2.3/config/boot.rb9
-rw-r--r--test/rails/app-1.2.3/config/database.yml12
-rw-r--r--test/rails/app-1.2.3/config/environment.rb11
-rw-r--r--test/rails/app-1.2.3/config/environments/development.rb7
-rw-r--r--test/rails/app-1.2.3/config/environments/production.rb3
-rw-r--r--test/rails/app-1.2.3/config/routes.rb4
-rw-r--r--test/rails/app-1.2.3/db/.gitignore0
-rw-r--r--test/rails/app-1.2.3/log/.gitignore1
-rw-r--r--test/rails/app-1.2.3/public/404.html1
-rw-r--r--test/rails/app-1.2.3/public/500.html1
-rw-r--r--test/rails/app-2.0.2/.gitignore2
-rw-r--r--test/rails/app-2.0.2/Rakefile7
-rw-r--r--test/rails/app-2.0.2/app/controllers/application.rb2
-rw-r--r--test/rails/app-2.0.2/app/controllers/foo_controller.rb34
-rw-r--r--test/rails/app-2.0.2/app/helpers/application_helper.rb2
-rw-r--r--test/rails/app-2.0.2/config/boot.rb9
-rw-r--r--test/rails/app-2.0.2/config/database.yml12
-rw-r--r--test/rails/app-2.0.2/config/environment.rb15
-rw-r--r--test/rails/app-2.0.2/config/environments/development.rb6
-rw-r--r--test/rails/app-2.0.2/config/environments/production.rb3
-rw-r--r--test/rails/app-2.0.2/config/routes.rb4
-rw-r--r--test/rails/app-2.0.2/db/.gitignore0
-rw-r--r--test/rails/app-2.0.2/log/.gitignore1
-rw-r--r--test/rails/app-2.0.2/public/404.html1
-rw-r--r--test/rails/app-2.0.2/public/500.html1
-rw-r--r--test/rails/app-2.1.2/.gitignore2
-rw-r--r--test/rails/app-2.1.2/Rakefile7
-rw-r--r--test/rails/app-2.1.2/app/controllers/application.rb2
-rw-r--r--test/rails/app-2.1.2/app/controllers/foo_controller.rb34
-rw-r--r--test/rails/app-2.1.2/app/helpers/application_helper.rb2
-rw-r--r--test/rails/app-2.1.2/config/boot.rb109
-rw-r--r--test/rails/app-2.1.2/config/database.yml12
-rw-r--r--test/rails/app-2.1.2/config/environment.rb15
-rw-r--r--test/rails/app-2.1.2/config/environments/development.rb5
-rw-r--r--test/rails/app-2.1.2/config/environments/production.rb3
-rw-r--r--test/rails/app-2.1.2/config/routes.rb4
-rw-r--r--test/rails/app-2.1.2/db/.gitignore0
-rw-r--r--test/rails/app-2.1.2/log/.gitignore1
-rw-r--r--test/rails/app-2.1.2/public/404.html1
-rw-r--r--test/rails/app-2.1.2/public/500.html1
-rw-r--r--test/rails/app-2.2.2/.gitignore2
-rw-r--r--test/rails/app-2.2.2/Rakefile7
-rw-r--r--test/rails/app-2.2.2/app/controllers/application.rb2
-rw-r--r--test/rails/app-2.2.2/app/controllers/foo_controller.rb34
-rw-r--r--test/rails/app-2.2.2/app/helpers/application_helper.rb2
-rw-r--r--test/rails/app-2.2.2/config/boot.rb109
-rw-r--r--test/rails/app-2.2.2/config/database.yml12
-rw-r--r--test/rails/app-2.2.2/config/environment.rb15
-rw-r--r--test/rails/app-2.2.2/config/environments/development.rb5
-rw-r--r--test/rails/app-2.2.2/config/environments/production.rb3
-rw-r--r--test/rails/app-2.2.2/config/routes.rb4
-rw-r--r--test/rails/app-2.2.2/db/.gitignore0
-rw-r--r--test/rails/app-2.2.2/log/.gitignore1
-rw-r--r--test/rails/app-2.2.2/public/404.html1
-rw-r--r--test/rails/app-2.2.2/public/500.html1
-rw-r--r--test/rails/app-2.3.2.1/.gitignore2
-rw-r--r--test/rails/app-2.3.2.1/Rakefile7
-rw-r--r--test/rails/app-2.3.2.1/app/controllers/application_controller.rb3
-rw-r--r--test/rails/app-2.3.2.1/app/controllers/foo_controller.rb34
-rw-r--r--test/rails/app-2.3.2.1/app/helpers/application_helper.rb2
-rw-r--r--test/rails/app-2.3.2.1/config/boot.rb107
-rw-r--r--test/rails/app-2.3.2.1/config/database.yml12
-rw-r--r--test/rails/app-2.3.2.1/config/environment.rb15
-rw-r--r--test/rails/app-2.3.2.1/config/environments/development.rb5
-rw-r--r--test/rails/app-2.3.2.1/config/environments/production.rb4
-rw-r--r--test/rails/app-2.3.2.1/config/routes.rb4
-rw-r--r--test/rails/app-2.3.2.1/db/.gitignore0
-rw-r--r--test/rails/app-2.3.2.1/log/.gitignore1
-rw-r--r--test/rails/app-2.3.2.1/public/404.html1
-rw-r--r--test/rails/app-2.3.2.1/public/500.html1
-rw-r--r--test/rails/test_rails.rb247
-rw-r--r--test/test_helper.rb169
-rw-r--r--test/tools/trickletest.rb45
-rw-r--r--test/unit/test_configurator.rb65
-rw-r--r--test/unit/test_http_parser.rb234
-rw-r--r--test/unit/test_request.rb159
-rw-r--r--test/unit/test_response.rb62
-rw-r--r--test/unit/test_server.rb66
-rw-r--r--test/unit/test_signals.rb191
-rw-r--r--test/unit/test_socket_helper.rb131
-rw-r--r--test/unit/test_upload.rb120
-rw-r--r--test/unit/test_util.rb87
88 files changed, 2506 insertions, 231 deletions
diff --git a/test/exec/test_exec.rb b/test/exec/test_exec.rb
index 712037c..014b270 100644
--- a/test/exec/test_exec.rb
+++ b/test/exec/test_exec.rb
@@ -1,43 +1,34 @@
 # 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
-DEFAULT_TRIES = 1000
-DEFAULT_RES = 0.2
-
 $unicorn_bin = ENV['UNICORN_TEST_BIN'] || "unicorn"
 redirect_test_io do
   do_test = system($unicorn_bin, '-v')
 end
 
 unless do_test
-  STDERR.puts "#{$unicorn_bin} not found in PATH=#{ENV['PATH']}, " \
-              "skipping this test"
+  warn "#{$unicorn_bin} not found in PATH=#{ENV['PATH']}, " \
+       "skipping this test"
 end
 
-begin
-  require 'rack'
-rescue LoadError
-  STDERR.puts "Unable to load Rack, skipping this test"
+unless try_require('rack')
+  warn "Unable to load Rack, skipping this test"
   do_test = false
 end
 
 class ExecTest < Test::Unit::TestCase
-  trap('QUIT', 'IGNORE')
+  trap(:QUIT, 'IGNORE')
 
   HI = <<-EOS
 use Rack::ContentLength
-run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, "HI\\n" ] }
+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" ]
+    [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ]
   end
 end
   EOS
@@ -47,10 +38,9 @@ end
   HEAVY_CFG = <<-EOS
 worker_processes 4
 timeout 30
-backlog 128
 logger Logger.new('#{COMMON_TMP.path}')
-before_fork do |server, worker_nr|
-  server.logger.info "before_fork: worker=\#{worker_nr}"
+before_fork do |server, worker|
+  server.logger.info "before_fork: worker=\#{worker.nr}"
 end
   EOS
 
@@ -82,6 +72,22 @@ end
     end
   end
 
+  def test_exit_signals
+    %w(INT TERM QUIT).each do |sig|
+      File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
+      pid = xfork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
+      wait_master_ready("test_stderr.#{pid}.log")
+      status = nil
+      assert_nothing_raised do
+        Process.kill(sig, pid)
+        pid, status = Process.waitpid2(pid)
+      end
+      reaped = File.readlines("test_stderr.#{pid}.log").grep(/reaped/)
+      assert_equal 1, reaped.size
+      assert status.exited?
+    end
+  end
+
   def test_basic
     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
     pid = fork do
@@ -92,6 +98,28 @@ end
     assert_shutdown(pid)
   end
 
+  def test_ttin_ttou
+    File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
+    pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
+    log = "test_stderr.#{pid}.log"
+    wait_master_ready(log)
+    [ 2, 3].each { |i|
+      assert_nothing_raised { Process.kill(:TTIN, pid) }
+      wait_workers_ready(log, i)
+    }
+    File.truncate(log, 0)
+    reaped = nil
+    [ 2, 1, 0].each { |i|
+      assert_nothing_raised { Process.kill(:TTOU, pid) }
+      DEFAULT_TRIES.times {
+        sleep DEFAULT_RES
+        reaped = File.readlines(log).grep(/reaped.*\s*worker=#{i}$/)
+        break if reaped.size == 1
+      }
+      assert_equal 1, reaped.size
+    }
+  end
+
   def test_help
     redirect_test_io do
       assert(system($unicorn_bin, "-h"), "help text returns true")
@@ -113,7 +141,7 @@ end
     pid_file = "#{@tmpdir}/test.pid"
     old_file = "#{pid_file}.oldbin"
     ucfg = Tempfile.new('unicorn_test_config')
-    ucfg.syswrite("listeners %w(#{@addr}:#{@port})\n")
+    ucfg.syswrite("listen %(#@addr:#@port)\n")
     ucfg.syswrite("pid %(#{pid_file})\n")
     ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n")
     pid = xfork do
@@ -126,14 +154,16 @@ end
 
     wait_for_file(pid_file)
     Process.waitpid(pid)
-    Process.kill('USR2', File.read(pid_file).to_i)
+    Process.kill(:USR2, File.read(pid_file).to_i)
     wait_for_file(old_file)
     wait_for_file(pid_file)
-    Process.kill('QUIT', File.read(old_file).to_i)
+    old_pid = File.read(old_file).to_i
+    Process.kill(:QUIT, old_pid)
+    wait_for_death(old_pid)
 
     ucfg.syswrite("timeout %(#{pid_file})\n") # introduce a bug
     current_pid = File.read(pid_file).to_i
-    Process.kill('USR2', current_pid)
+    Process.kill(:USR2, current_pid)
 
     # wait for pid_file to restore itself
     tries = DEFAULT_TRIES
@@ -156,9 +186,11 @@ end
     # fix the bug
     ucfg.sysseek(0)
     ucfg.truncate(0)
-    ucfg.syswrite("listeners %w(#{@addr}:#{@port} #{@addr}:#{port2})\n")
+    ucfg.syswrite("listen %(#@addr:#@port)\n")
+    ucfg.syswrite("listen %(#@addr:#{port2})\n")
     ucfg.syswrite("pid %(#{pid_file})\n")
-    Process.kill('USR2', current_pid)
+    assert_nothing_raised { Process.kill(:USR2, current_pid) }
+
     wait_for_file(old_file)
     wait_for_file(pid_file)
     new_pid = File.read(pid_file).to_i
@@ -170,8 +202,8 @@ end
     assert_equal String, results[1].class
 
     assert_nothing_raised do
-      Process.kill('QUIT', current_pid)
-      Process.kill('QUIT', new_pid)
+      Process.kill(:QUIT, current_pid)
+      Process.kill(:QUIT, new_pid)
     end
   end
 
@@ -192,14 +224,16 @@ end
 
     wait_for_file(pid_file)
     Process.waitpid(pid)
-    Process.kill('USR2', File.read(pid_file).to_i)
+    Process.kill(:USR2, File.read(pid_file).to_i)
     wait_for_file(old_file)
     wait_for_file(pid_file)
-    Process.kill('QUIT', File.read(old_file).to_i)
+    old_pid = File.read(old_file).to_i
+    Process.kill(:QUIT, old_pid)
+    wait_for_death(old_pid)
 
     File.unlink("config.ru") # break reloading
     current_pid = File.read(pid_file).to_i
-    Process.kill('USR2', current_pid)
+    Process.kill(:USR2, current_pid)
 
     # wait for pid_file to restore itself
     tries = DEFAULT_TRIES
@@ -210,17 +244,17 @@ end
     rescue Errno::ENOENT
       (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry
     end
-    assert_equal current_pid, File.read(pid_file).to_i
 
     tries = DEFAULT_TRIES
     while File.exist?(old_file)
       (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break
     end
     assert ! File.exist?(old_file), "oldbin=#{old_file} gone"
+    assert_equal current_pid, File.read(pid_file).to_i
 
     # fix the bug
     File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
-    Process.kill('USR2', current_pid)
+    assert_nothing_raised { Process.kill(:USR2, current_pid) }
     wait_for_file(old_file)
     wait_for_file(pid_file)
     new_pid = File.read(pid_file).to_i
@@ -230,25 +264,86 @@ end
     assert_equal String, results[0].class
 
     assert_nothing_raised do
-      Process.kill('QUIT', current_pid)
-      Process.kill('QUIT', new_pid)
+      Process.kill(:QUIT, current_pid)
+      Process.kill(:QUIT, new_pid)
     end
   end
 
-  def test_unicorn_config_listeners_overrides_cli
-    port2 = unused_port(@addr)
+  def test_unicorn_config_listener_swap
+    port_cli = unused_port
     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")
+    ucfg.syswrite("listen '#@addr:#@port'\n")
     pid = xfork do
       redirect_test_io do
-        exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}")
+        exec($unicorn_bin, "-c#{ucfg.path}", "-l#@addr:#{port_cli}")
       end
     end
+    results = retry_hit(["http://#@addr:#{port_cli}/"])
+    assert_equal String, results[0].class
+    results = retry_hit(["http://#@addr:#@port/"])
+    assert_equal String, results[0].class
+
+    port2 = unused_port(@addr)
+    ucfg.sysseek(0)
+    ucfg.truncate(0)
+    ucfg.syswrite("listen '#@addr:#{port2}'\n")
+    Process.kill(:HUP, pid)
+
+    results = retry_hit(["http://#@addr:#{port2}/"])
+    assert_equal String, results[0].class
+    results = retry_hit(["http://#@addr:#{port_cli}/"])
+    assert_equal String, results[0].class
+    assert_nothing_raised do
+      reuse = TCPServer.new(@addr, @port)
+      reuse.close
+    end
+    assert_shutdown(pid)
+  end
+
+  def test_unicorn_config_listen_with_options
+    File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
+    ucfg = Tempfile.new('unicorn_test_config')
+    ucfg.syswrite("listen '#{@addr}:#{@port}', :backlog => 512,\n")
+    ucfg.syswrite("                            :rcvbuf => 4096,\n")
+    ucfg.syswrite("                            :sndbuf => 4096\n")
+    pid = xfork do
+      redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
+    end
+    results = retry_hit(["http://#{@addr}:#{@port}/"])
+    assert_equal String, results[0].class
+    assert_shutdown(pid)
+  end
+
+  def test_unicorn_config_per_worker_listen
+    port2 = unused_port
+    pid_spit = 'use Rack::ContentLength;' \
+      'run proc { |e| [ 200, {"Content-Type"=>"text/plain"}, ["#$$\\n"] ] }'
+    File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) }
+    tmp = Tempfile.new('test.socket')
+    File.unlink(tmp.path)
+    ucfg = Tempfile.new('unicorn_test_config')
+    ucfg.syswrite("listen '#@addr:#@port'\n")
+    ucfg.syswrite("before_fork { |s,w|\n")
+    ucfg.syswrite("  s.listen('#{tmp.path}', :backlog => 5, :sndbuf => 8192)\n")
+    ucfg.syswrite("  s.listen('#@addr:#{port2}', :rcvbuf => 8192)\n")
+    ucfg.syswrite("\n}\n")
+    pid = xfork do
+      redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") }
+    end
     results = retry_hit(["http://#{@addr}:#{@port}/"])
-    assert_raises(Errno::ECONNREFUSED) { TCPSocket.new(@addr, port2) }
     assert_equal String, results[0].class
+    worker_pid = results[0].to_i
+    assert_not_equal pid, worker_pid
+    s = UNIXSocket.new(tmp.path)
+    s.syswrite("GET / HTTP/1.0\r\n\r\n")
+    results = ''
+    loop { results << s.sysread(4096) } rescue nil
+    assert_nothing_raised { s.close }
+    assert_equal worker_pid, results.split(/\r\n/).last.to_i
+    results = hit(["http://#@addr:#{port2}/"])
+    assert_equal String, results[0].class
+    assert_equal worker_pid, results[0].to_i
     assert_shutdown(pid)
   end
 
@@ -283,34 +378,36 @@ end
     results = retry_hit(["http://#{@addr}:#{@port}/"])
     assert_equal String, results[0].class
     wait_master_ready(COMMON_TMP.path)
+    wait_workers_ready(COMMON_TMP.path, 4)
     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)
+      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 = DEFAULT_TRIES
     log = File.readlines(rotate.path)
-    while (tries -= 1) > 0 && log.grep(/rotating logs\.\.\./).size < 4
+    while (tries -= 1) > 0 &&
+          log.grep(/reopening logs\.\.\./).size < 5
       sleep DEFAULT_RES
       log = File.readlines(rotate.path)
     end
-    assert_equal 4, log.grep(/rotating logs\.\.\./).size
-    assert_equal 0, log.grep(/done rotating logs/).size
+    assert_equal 5, log.grep(/reopening logs\.\.\./).size
+    assert_equal 0, log.grep(/done reopening logs/).size
 
     tries = DEFAULT_TRIES
     log = File.readlines(COMMON_TMP.path)
-    while (tries -= 1) > 0 && log.grep(/done rotating logs/).size < 4
+    while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < 5
       sleep DEFAULT_RES
       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) }
+    assert_equal 5, log.grep(/done reopening logs/).size
+    assert_equal 0, log.grep(/reopening logs\.\.\./).size
+    assert_nothing_raised { Process.kill(:QUIT, pid) }
     status = nil
     assert_nothing_raised { pid, status = Process.waitpid2(pid) }
     assert status.success?, "exited successfully"
@@ -380,6 +477,39 @@ end
     reexec_basic_test(pid, pid_file)
   end
 
+  def test_socket_unlinked_restore
+    results = nil
+    sock = Tempfile.new('unicorn_test_sock')
+    sock_path = sock.path
+    @sockets << sock_path
+    sock.close!
+    ucfg = Tempfile.new('unicorn_test_config')
+    ucfg.syswrite("listen \"#{sock_path}\"\n")
+
+    File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
+    pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } }
+    wait_for_file(sock_path)
+    assert File.socket?(sock_path)
+    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
+    assert_nothing_raised do
+      File.unlink(sock_path)
+      Process.kill(:HUP, pid)
+    end
+    wait_for_file(sock_path)
+    assert File.socket?(sock_path)
+    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
+  end
+
   def test_unicorn_config_file
     pid_file = "#{@tmpdir}/test.pid"
     sock = Tempfile.new('unicorn_test_sock')
@@ -415,7 +545,7 @@ end
     assert_equal String, results.class
 
     # try reloading the config
-    sock = Tempfile.new('unicorn_test_sock')
+    sock = Tempfile.new('new_test_sock')
     new_sock_path = sock.path
     @sockets << new_sock_path
     sock.close!
@@ -425,11 +555,12 @@ end
 
     assert_nothing_raised do
       ucfg = File.open(ucfg.path, "wb")
+      ucfg.syswrite("listen \"#{sock_path}\"\n")
       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)
+      Process.kill(:HUP, pid)
     end
 
     wait_for_file(new_sock_path)
@@ -472,99 +603,72 @@ end
     reexec_usr2_quit_test(new_pid, pid_file)
   end
 
-  private
+  def test_reexec_fd_leak
+    unless RUBY_PLATFORM =~ /linux/ # Solaris may work, too, but I forget...
+      warn "FD leak test only works on Linux at the moment"
+      return
+    end
+    pid_file = "#{@tmpdir}/test.pid"
+    log = Tempfile.new('unicorn_test_log')
+    log.sync = true
+    ucfg = Tempfile.new('unicorn_test_config')
+    ucfg.syswrite("pid \"#{pid_file}\"\n")
+    ucfg.syswrite("logger Logger.new('#{log.path}')\n")
+    ucfg.syswrite("stderr_path '#{log.path}'\n")
+    ucfg.syswrite("stdout_path '#{log.path}'\n")
+    ucfg.close
 
-    # sometimes the server may not come up right away
-    def retry_hit(uris = [])
-      tries = DEFAULT_TRIES
-      begin
-        hit(uris)
-      rescue Errno::ECONNREFUSED => err
-        if (tries -= 1) > 0
-          sleep DEFAULT_RES
-          retry
-        end
-        raise err
+    File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
+    pid = xfork do
+      redirect_test_io do
+        exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}")
       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
+    wait_master_ready(log.path)
+    File.truncate(log.path, 0)
+    wait_for_file(pid_file)
+    orig_pid = pid = File.read(pid_file).to_i
+    orig_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
+    assert $?.success?
+    expect_size = orig_fds.size
 
-    def wait_master_ready(master_log)
-      tries = DEFAULT_TRIES
-      while (tries -= 1) > 0
-        begin
-          File.readlines(master_log).grep(/master process ready/)[0] and return
-        rescue Errno::ENOENT
-        end
-        sleep DEFAULT_RES
-      end
-      raise "master process never became ready"
+    assert_nothing_raised do
+      Process.kill(:USR2, pid)
+      wait_for_file("#{pid_file}.oldbin")
+      Process.kill(:QUIT, pid)
     end
+    wait_for_death(pid)
+
+    wait_master_ready(log.path)
+    File.truncate(log.path, 0)
+    wait_for_file(pid_file)
+    pid = File.read(pid_file).to_i
+    assert_not_equal orig_pid, pid
+    curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
+    assert $?.success?
 
-    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}/"]) }
+    # we could've inherited descriptors the first time around
+    assert expect_size >= curr_fds.size, curr_fds.inspect
+    expect_size = curr_fds.size
+
+    assert_nothing_raised do
+      Process.kill(:USR2, pid)
       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) }
+      Process.kill(:QUIT, pid)
     end
+    wait_for_death(pid)
 
-    def wait_for_file(path)
-      tries = DEFAULT_TRIES
-      while (tries -= 1) > 0 && ! File.exist?(path)
-        sleep DEFAULT_RES
-      end
-      assert File.exist?(path), "path=#{path} exists"
-    end
+    wait_master_ready(log.path)
+    File.truncate(log.path, 0)
+    wait_for_file(pid_file)
+    pid = File.read(pid_file).to_i
+    curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/)
+    assert $?.success?
+    assert_equal expect_size, curr_fds.size, curr_fds.inspect
 
-    def xfork(&block)
-      fork do
-        ObjectSpace.each_object(Tempfile) do |tmp|
-          ObjectSpace.undefine_finalizer(tmp)
-        end
-        yield
-      end
-    end
+    Process.kill(:QUIT, pid)
+    wait_for_death(pid)
+  end
 
 end if do_test
diff --git a/test/rails/app-1.2.3/.gitignore b/test/rails/app-1.2.3/.gitignore
new file mode 100644
index 0000000..f451f91
--- /dev/null
+++ b/test/rails/app-1.2.3/.gitignore
@@ -0,0 +1,2 @@
+/tmp
+/vendor
diff --git a/test/rails/app-1.2.3/Rakefile b/test/rails/app-1.2.3/Rakefile
new file mode 100644
index 0000000..fbebfca
--- /dev/null
+++ b/test/rails/app-1.2.3/Rakefile
@@ -0,0 +1,7 @@
+require(File.join(File.dirname(__FILE__), 'config', 'boot'))
+
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+require 'tasks/rails'
diff --git a/test/rails/app-1.2.3/app/controllers/application.rb b/test/rails/app-1.2.3/app/controllers/application.rb
new file mode 100644
index 0000000..ae8cac0
--- /dev/null
+++ b/test/rails/app-1.2.3/app/controllers/application.rb
@@ -0,0 +1,4 @@
+class ApplicationController < ActionController::Base
+  # Pick a unique cookie name to distinguish our session data from others'
+  session :session_key => "_unicorn_rails_test.#{rand}"
+end
diff --git a/test/rails/app-1.2.3/app/controllers/foo_controller.rb b/test/rails/app-1.2.3/app/controllers/foo_controller.rb
new file mode 100644
index 0000000..8d877d1
--- /dev/null
+++ b/test/rails/app-1.2.3/app/controllers/foo_controller.rb
@@ -0,0 +1,34 @@
+require 'digest/sha1'
+class FooController < ApplicationController
+  def index
+    render :text => "FOO\n"
+  end
+
+  def xcookie
+    cookies["foo"] = "cookie #$$"
+    render :text => ""
+  end
+
+  def xnotice
+    flash[:notice] = "session #$$"
+    render :text => ""
+  end
+
+  def xpost
+    if request.post?
+      digest = Digest::SHA1.new
+      out = "params: #{params.inspect}\n"
+      if file = params[:file]
+        loop do
+          buf = file.read(4096) or break
+          digest.update(buf)
+        end
+        out << "sha1: #{digest.to_s}\n"
+      end
+      headers['content-type'] = 'text/plain'
+      render :text => out
+    else
+      render :status => 403, :text => "need post\n"
+    end
+  end
+end
diff --git a/test/rails/app-1.2.3/app/helpers/application_helper.rb b/test/rails/app-1.2.3/app/helpers/application_helper.rb
new file mode 100644
index 0000000..de6be79
--- /dev/null
+++ b/test/rails/app-1.2.3/app/helpers/application_helper.rb
@@ -0,0 +1,2 @@
+module ApplicationHelper
+end
diff --git a/test/rails/app-1.2.3/config/boot.rb b/test/rails/app-1.2.3/config/boot.rb
new file mode 100644
index 0000000..71c7d7c
--- /dev/null
+++ b/test/rails/app-1.2.3/config/boot.rb
@@ -0,0 +1,9 @@
+unless defined?(RAILS_ROOT)
+  root_path = File.join(File.dirname(__FILE__), '..')
+  RAILS_ROOT = root_path
+end
+
+unless defined?(Rails::Initializer)
+  require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
+  Rails::Initializer.run(:set_load_path)
+end
diff --git a/test/rails/app-1.2.3/config/database.yml b/test/rails/app-1.2.3/config/database.yml
new file mode 100644
index 0000000..9f77843
--- /dev/null
+++ b/test/rails/app-1.2.3/config/database.yml
@@ -0,0 +1,12 @@
+development:
+  adapter: sqlite3
+  database: db/development.sqlite3
+  timeout: 5000
+test:
+  adapter: sqlite3
+  database: db/test.sqlite3
+  timeout: 5000
+production:
+  adapter: sqlite3
+  database: db/production.sqlite3
+  timeout: 5000
diff --git a/test/rails/app-1.2.3/config/environment.rb b/test/rails/app-1.2.3/config/environment.rb
new file mode 100644
index 0000000..2ef6b4a
--- /dev/null
+++ b/test/rails/app-1.2.3/config/environment.rb
@@ -0,0 +1,11 @@
+unless defined? RAILS_GEM_VERSION
+  RAILS_GEM_VERSION = ENV['UNICORN_RAILS_VERSION'] # || '1.2.3'
+end
+
+# Bootstrap the Rails environment, frameworks, and default configuration
+require File.join(File.dirname(__FILE__), 'boot')
+
+Rails::Initializer.run do |config|
+  config.frameworks -= [ :action_web_service, :action_mailer ]
+  config.action_controller.session_store = :active_record_store
+end
diff --git a/test/rails/app-1.2.3/config/environments/development.rb b/test/rails/app-1.2.3/config/environments/development.rb
new file mode 100644
index 0000000..032fb46
--- /dev/null
+++ b/test/rails/app-1.2.3/config/environments/development.rb
@@ -0,0 +1,7 @@
+config.cache_classes = false
+config.whiny_nils = true
+config.breakpoint_server = true
+config.action_controller.consider_all_requests_local = true
+config.action_controller.perform_caching = false
+config.action_view.cache_template_extensions = false
+config.action_view.debug_rjs = true
diff --git a/test/rails/app-1.2.3/config/environments/production.rb b/test/rails/app-1.2.3/config/environments/production.rb
new file mode 100644
index 0000000..c4059e3
--- /dev/null
+++ b/test/rails/app-1.2.3/config/environments/production.rb
@@ -0,0 +1,3 @@
+config.cache_classes = true
+config.action_controller.consider_all_requests_local = false
+config.action_controller.perform_caching             = true
diff --git a/test/rails/app-1.2.3/config/routes.rb b/test/rails/app-1.2.3/config/routes.rb
new file mode 100644
index 0000000..774028f
--- /dev/null
+++ b/test/rails/app-1.2.3/config/routes.rb
@@ -0,0 +1,4 @@
+ActionController::Routing::Routes.draw do |map|
+  map.connect ':controller/:action/:id.:format'
+  map.connect ':controller/:action/:id'
+end
diff --git a/test/rails/app-1.2.3/db/.gitignore b/test/rails/app-1.2.3/db/.gitignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/rails/app-1.2.3/db/.gitignore
diff --git a/test/rails/app-1.2.3/log/.gitignore b/test/rails/app-1.2.3/log/.gitignore
new file mode 100644
index 0000000..397b4a7
--- /dev/null
+++ b/test/rails/app-1.2.3/log/.gitignore
@@ -0,0 +1 @@
+*.log
diff --git a/test/rails/app-1.2.3/public/404.html b/test/rails/app-1.2.3/public/404.html
new file mode 100644
index 0000000..44d986c
--- /dev/null
+++ b/test/rails/app-1.2.3/public/404.html
@@ -0,0 +1 @@
+404 Not Found
diff --git a/test/rails/app-1.2.3/public/500.html b/test/rails/app-1.2.3/public/500.html
new file mode 100644
index 0000000..e534a49
--- /dev/null
+++ b/test/rails/app-1.2.3/public/500.html
@@ -0,0 +1 @@
+500 Internal Server Error
diff --git a/test/rails/app-2.0.2/.gitignore b/test/rails/app-2.0.2/.gitignore
new file mode 100644
index 0000000..f451f91
--- /dev/null
+++ b/test/rails/app-2.0.2/.gitignore
@@ -0,0 +1,2 @@
+/tmp
+/vendor
diff --git a/test/rails/app-2.0.2/Rakefile b/test/rails/app-2.0.2/Rakefile
new file mode 100644
index 0000000..fbebfca
--- /dev/null
+++ b/test/rails/app-2.0.2/Rakefile
@@ -0,0 +1,7 @@
+require(File.join(File.dirname(__FILE__), 'config', 'boot'))
+
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+require 'tasks/rails'
diff --git a/test/rails/app-2.0.2/app/controllers/application.rb b/test/rails/app-2.0.2/app/controllers/application.rb
new file mode 100644
index 0000000..09705d1
--- /dev/null
+++ b/test/rails/app-2.0.2/app/controllers/application.rb
@@ -0,0 +1,2 @@
+class ApplicationController < ActionController::Base
+end
diff --git a/test/rails/app-2.0.2/app/controllers/foo_controller.rb b/test/rails/app-2.0.2/app/controllers/foo_controller.rb
new file mode 100644
index 0000000..8d877d1
--- /dev/null
+++ b/test/rails/app-2.0.2/app/controllers/foo_controller.rb
@@ -0,0 +1,34 @@
+require 'digest/sha1'
+class FooController < ApplicationController
+  def index
+    render :text => "FOO\n"
+  end
+
+  def xcookie
+    cookies["foo"] = "cookie #$$"
+    render :text => ""
+  end
+
+  def xnotice
+    flash[:notice] = "session #$$"
+    render :text => ""
+  end
+
+  def xpost
+    if request.post?
+      digest = Digest::SHA1.new
+      out = "params: #{params.inspect}\n"
+      if file = params[:file]
+        loop do
+          buf = file.read(4096) or break
+          digest.update(buf)
+        end
+        out << "sha1: #{digest.to_s}\n"
+      end
+      headers['content-type'] = 'text/plain'
+      render :text => out
+    else
+      render :status => 403, :text => "need post\n"
+    end
+  end
+end
diff --git a/test/rails/app-2.0.2/app/helpers/application_helper.rb b/test/rails/app-2.0.2/app/helpers/application_helper.rb
new file mode 100644
index 0000000..de6be79
--- /dev/null
+++ b/test/rails/app-2.0.2/app/helpers/application_helper.rb
@@ -0,0 +1,2 @@
+module ApplicationHelper
+end
diff --git a/test/rails/app-2.0.2/config/boot.rb b/test/rails/app-2.0.2/config/boot.rb
new file mode 100644
index 0000000..71c7d7c
--- /dev/null
+++ b/test/rails/app-2.0.2/config/boot.rb
@@ -0,0 +1,9 @@
+unless defined?(RAILS_ROOT)
+  root_path = File.join(File.dirname(__FILE__), '..')
+  RAILS_ROOT = root_path
+end
+
+unless defined?(Rails::Initializer)
+  require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
+  Rails::Initializer.run(:set_load_path)
+end
diff --git a/test/rails/app-2.0.2/config/database.yml b/test/rails/app-2.0.2/config/database.yml
new file mode 100644
index 0000000..9f77843
--- /dev/null
+++ b/test/rails/app-2.0.2/config/database.yml
@@ -0,0 +1,12 @@
+development:
+  adapter: sqlite3
+  database: db/development.sqlite3
+  timeout: 5000
+test:
+  adapter: sqlite3
+  database: db/test.sqlite3
+  timeout: 5000
+production:
+  adapter: sqlite3
+  database: db/production.sqlite3
+  timeout: 5000
diff --git a/test/rails/app-2.0.2/config/environment.rb b/test/rails/app-2.0.2/config/environment.rb
new file mode 100644
index 0000000..7c720f6
--- /dev/null
+++ b/test/rails/app-2.0.2/config/environment.rb
@@ -0,0 +1,15 @@
+unless defined? RAILS_GEM_VERSION
+  RAILS_GEM_VERSION = ENV['UNICORN_RAILS_VERSION']
+end
+
+# Bootstrap the Rails environment, frameworks, and default configuration
+require File.join(File.dirname(__FILE__), 'boot')
+
+Rails::Initializer.run do |config|
+  config.frameworks -= [ :action_web_service, :action_mailer ]
+  config.action_controller.session_store = :active_record_store
+  config.action_controller.session = {
+    :session_key => "_unicorn_rails_test.#{rand}",
+    :secret => "#{rand}#{rand}#{rand}#{rand}",
+  }
+end
diff --git a/test/rails/app-2.0.2/config/environments/development.rb b/test/rails/app-2.0.2/config/environments/development.rb
new file mode 100644
index 0000000..6a613c1
--- /dev/null
+++ b/test/rails/app-2.0.2/config/environments/development.rb
@@ -0,0 +1,6 @@
+config.cache_classes = false
+config.whiny_nils = true
+config.action_controller.consider_all_requests_local = true
+config.action_controller.perform_caching = false
+config.action_view.cache_template_extensions = false
+config.action_view.debug_rjs = true
diff --git a/test/rails/app-2.0.2/config/environments/production.rb b/test/rails/app-2.0.2/config/environments/production.rb
new file mode 100644
index 0000000..c4059e3
--- /dev/null
+++ b/test/rails/app-2.0.2/config/environments/production.rb
@@ -0,0 +1,3 @@
+config.cache_classes = true
+config.action_controller.consider_all_requests_local = false
+config.action_controller.perform_caching             = true
diff --git a/test/rails/app-2.0.2/config/routes.rb b/test/rails/app-2.0.2/config/routes.rb
new file mode 100644
index 0000000..774028f
--- /dev/null
+++ b/test/rails/app-2.0.2/config/routes.rb
@@ -0,0 +1,4 @@
+ActionController::Routing::Routes.draw do |map|
+  map.connect ':controller/:action/:id.:format'
+  map.connect ':controller/:action/:id'
+end
diff --git a/test/rails/app-2.0.2/db/.gitignore b/test/rails/app-2.0.2/db/.gitignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/rails/app-2.0.2/db/.gitignore
diff --git a/test/rails/app-2.0.2/log/.gitignore b/test/rails/app-2.0.2/log/.gitignore
new file mode 100644
index 0000000..397b4a7
--- /dev/null
+++ b/test/rails/app-2.0.2/log/.gitignore
@@ -0,0 +1 @@
+*.log
diff --git a/test/rails/app-2.0.2/public/404.html b/test/rails/app-2.0.2/public/404.html
new file mode 100644
index 0000000..44d986c
--- /dev/null
+++ b/test/rails/app-2.0.2/public/404.html
@@ -0,0 +1 @@
+404 Not Found
diff --git a/test/rails/app-2.0.2/public/500.html b/test/rails/app-2.0.2/public/500.html
new file mode 100644
index 0000000..e534a49
--- /dev/null
+++ b/test/rails/app-2.0.2/public/500.html
@@ -0,0 +1 @@
+500 Internal Server Error
diff --git a/test/rails/app-2.1.2/.gitignore b/test/rails/app-2.1.2/.gitignore
new file mode 100644
index 0000000..f451f91
--- /dev/null
+++ b/test/rails/app-2.1.2/.gitignore
@@ -0,0 +1,2 @@
+/tmp
+/vendor
diff --git a/test/rails/app-2.1.2/Rakefile b/test/rails/app-2.1.2/Rakefile
new file mode 100644
index 0000000..fbebfca
--- /dev/null
+++ b/test/rails/app-2.1.2/Rakefile
@@ -0,0 +1,7 @@
+require(File.join(File.dirname(__FILE__), 'config', 'boot'))
+
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+require 'tasks/rails'
diff --git a/test/rails/app-2.1.2/app/controllers/application.rb b/test/rails/app-2.1.2/app/controllers/application.rb
new file mode 100644
index 0000000..09705d1
--- /dev/null
+++ b/test/rails/app-2.1.2/app/controllers/application.rb
@@ -0,0 +1,2 @@
+class ApplicationController < ActionController::Base
+end
diff --git a/test/rails/app-2.1.2/app/controllers/foo_controller.rb b/test/rails/app-2.1.2/app/controllers/foo_controller.rb
new file mode 100644
index 0000000..8d877d1
--- /dev/null
+++ b/test/rails/app-2.1.2/app/controllers/foo_controller.rb
@@ -0,0 +1,34 @@
+require 'digest/sha1'
+class FooController < ApplicationController
+  def index
+    render :text => "FOO\n"
+  end
+
+  def xcookie
+    cookies["foo"] = "cookie #$$"
+    render :text => ""
+  end
+
+  def xnotice
+    flash[:notice] = "session #$$"
+    render :text => ""
+  end
+
+  def xpost
+    if request.post?
+      digest = Digest::SHA1.new
+      out = "params: #{params.inspect}\n"
+      if file = params[:file]
+        loop do
+          buf = file.read(4096) or break
+          digest.update(buf)
+        end
+        out << "sha1: #{digest.to_s}\n"
+      end
+      headers['content-type'] = 'text/plain'
+      render :text => out
+    else
+      render :status => 403, :text => "need post\n"
+    end
+  end
+end
diff --git a/test/rails/app-2.1.2/app/helpers/application_helper.rb b/test/rails/app-2.1.2/app/helpers/application_helper.rb
new file mode 100644
index 0000000..de6be79
--- /dev/null
+++ b/test/rails/app-2.1.2/app/helpers/application_helper.rb
@@ -0,0 +1,2 @@
+module ApplicationHelper
+end
diff --git a/test/rails/app-2.1.2/config/boot.rb b/test/rails/app-2.1.2/config/boot.rb
new file mode 100644
index 0000000..0a51688
--- /dev/null
+++ b/test/rails/app-2.1.2/config/boot.rb
@@ -0,0 +1,109 @@
+# Don't change this file!
+# Configure your app in config/environment.rb and config/environments/*.rb
+
+RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
+
+module Rails
+  class << self
+    def boot!
+      unless booted?
+        preinitialize
+        pick_boot.run
+      end
+    end
+
+    def booted?
+      defined? Rails::Initializer
+    end
+
+    def pick_boot
+      (vendor_rails? ? VendorBoot : GemBoot).new
+    end
+
+    def vendor_rails?
+      File.exist?("#{RAILS_ROOT}/vendor/rails")
+    end
+
+    def preinitialize
+      load(preinitializer_path) if File.exist?(preinitializer_path)
+    end
+
+    def preinitializer_path
+      "#{RAILS_ROOT}/config/preinitializer.rb"
+    end
+  end
+
+  class Boot
+    def run
+      load_initializer
+      Rails::Initializer.run(:set_load_path)
+    end
+  end
+
+  class VendorBoot < Boot
+    def load_initializer
+      require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
+      Rails::Initializer.run(:install_gem_spec_stubs)
+    end
+  end
+
+  class GemBoot < Boot
+    def load_initializer
+      self.class.load_rubygems
+      load_rails_gem
+      require 'initializer'
+    end
+
+    def load_rails_gem
+      if version = self.class.gem_version
+        gem 'rails', version
+      else
+        gem 'rails'
+      end
+    rescue Gem::LoadError => load_error
+      $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.)
+      exit 1
+    end
+
+    class << self
+      def rubygems_version
+        Gem::RubyGemsVersion rescue nil
+      end
+
+      def gem_version
+        if defined? RAILS_GEM_VERSION
+          RAILS_GEM_VERSION
+        elsif ENV.include?('RAILS_GEM_VERSION')
+          ENV['RAILS_GEM_VERSION']
+        else
+          parse_gem_version(read_environment_rb)
+        end
+      end
+
+      def load_rubygems
+        require 'rubygems'
+        min_version = '1.3.1'
+        unless rubygems_version >= min_version
+          $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.)
+          exit 1
+        end
+
+      rescue LoadError
+        $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org)
+        exit 1
+      end
+
+      def parse_gem_version(text)
+        $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
+      end
+
+      private
+        def read_environment_rb
+          File.read("#{RAILS_ROOT}/config/environment.rb")
+        end
+    end
+  end
+end
+
+# All that for this:
+Rails.boot!
diff --git a/test/rails/app-2.1.2/config/database.yml b/test/rails/app-2.1.2/config/database.yml
new file mode 100644
index 0000000..9f77843
--- /dev/null
+++ b/test/rails/app-2.1.2/config/database.yml
@@ -0,0 +1,12 @@
+development:
+  adapter: sqlite3
+  database: db/development.sqlite3
+  timeout: 5000
+test:
+  adapter: sqlite3
+  database: db/test.sqlite3
+  timeout: 5000
+production:
+  adapter: sqlite3
+  database: db/production.sqlite3
+  timeout: 5000
diff --git a/test/rails/app-2.1.2/config/environment.rb b/test/rails/app-2.1.2/config/environment.rb
new file mode 100644
index 0000000..7c720f6
--- /dev/null
+++ b/test/rails/app-2.1.2/config/environment.rb
@@ -0,0 +1,15 @@
+unless defined? RAILS_GEM_VERSION
+  RAILS_GEM_VERSION = ENV['UNICORN_RAILS_VERSION']
+end
+
+# Bootstrap the Rails environment, frameworks, and default configuration
+require File.join(File.dirname(__FILE__), 'boot')
+
+Rails::Initializer.run do |config|
+  config.frameworks -= [ :action_web_service, :action_mailer ]
+  config.action_controller.session_store = :active_record_store
+  config.action_controller.session = {
+    :session_key => "_unicorn_rails_test.#{rand}",
+    :secret => "#{rand}#{rand}#{rand}#{rand}",
+  }
+end
diff --git a/test/rails/app-2.1.2/config/environments/development.rb b/test/rails/app-2.1.2/config/environments/development.rb
new file mode 100644
index 0000000..7f49032
--- /dev/null
+++ b/test/rails/app-2.1.2/config/environments/development.rb
@@ -0,0 +1,5 @@
+config.cache_classes = false
+config.whiny_nils = true
+config.action_controller.consider_all_requests_local = true
+config.action_controller.perform_caching = false
+config.action_view.debug_rjs = true
diff --git a/test/rails/app-2.1.2/config/environments/production.rb b/test/rails/app-2.1.2/config/environments/production.rb
new file mode 100644
index 0000000..c4059e3
--- /dev/null
+++ b/test/rails/app-2.1.2/config/environments/production.rb
@@ -0,0 +1,3 @@
+config.cache_classes = true
+config.action_controller.consider_all_requests_local = false
+config.action_controller.perform_caching             = true
diff --git a/test/rails/app-2.1.2/config/routes.rb b/test/rails/app-2.1.2/config/routes.rb
new file mode 100644
index 0000000..774028f
--- /dev/null
+++ b/test/rails/app-2.1.2/config/routes.rb
@@ -0,0 +1,4 @@
+ActionController::Routing::Routes.draw do |map|
+  map.connect ':controller/:action/:id.:format'
+  map.connect ':controller/:action/:id'
+end
diff --git a/test/rails/app-2.1.2/db/.gitignore b/test/rails/app-2.1.2/db/.gitignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/rails/app-2.1.2/db/.gitignore
diff --git a/test/rails/app-2.1.2/log/.gitignore b/test/rails/app-2.1.2/log/.gitignore
new file mode 100644
index 0000000..397b4a7
--- /dev/null
+++ b/test/rails/app-2.1.2/log/.gitignore
@@ -0,0 +1 @@
+*.log
diff --git a/test/rails/app-2.1.2/public/404.html b/test/rails/app-2.1.2/public/404.html
new file mode 100644
index 0000000..44d986c
--- /dev/null
+++ b/test/rails/app-2.1.2/public/404.html
@@ -0,0 +1 @@
+404 Not Found
diff --git a/test/rails/app-2.1.2/public/500.html b/test/rails/app-2.1.2/public/500.html
new file mode 100644
index 0000000..e534a49
--- /dev/null
+++ b/test/rails/app-2.1.2/public/500.html
@@ -0,0 +1 @@
+500 Internal Server Error
diff --git a/test/rails/app-2.2.2/.gitignore b/test/rails/app-2.2.2/.gitignore
new file mode 100644
index 0000000..f451f91
--- /dev/null
+++ b/test/rails/app-2.2.2/.gitignore
@@ -0,0 +1,2 @@
+/tmp
+/vendor
diff --git a/test/rails/app-2.2.2/Rakefile b/test/rails/app-2.2.2/Rakefile
new file mode 100644
index 0000000..fbebfca
--- /dev/null
+++ b/test/rails/app-2.2.2/Rakefile
@@ -0,0 +1,7 @@
+require(File.join(File.dirname(__FILE__), 'config', 'boot'))
+
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+require 'tasks/rails'
diff --git a/test/rails/app-2.2.2/app/controllers/application.rb b/test/rails/app-2.2.2/app/controllers/application.rb
new file mode 100644
index 0000000..09705d1
--- /dev/null
+++ b/test/rails/app-2.2.2/app/controllers/application.rb
@@ -0,0 +1,2 @@
+class ApplicationController < ActionController::Base
+end
diff --git a/test/rails/app-2.2.2/app/controllers/foo_controller.rb b/test/rails/app-2.2.2/app/controllers/foo_controller.rb
new file mode 100644
index 0000000..8d877d1
--- /dev/null
+++ b/test/rails/app-2.2.2/app/controllers/foo_controller.rb
@@ -0,0 +1,34 @@
+require 'digest/sha1'
+class FooController < ApplicationController
+  def index
+    render :text => "FOO\n"
+  end
+
+  def xcookie
+    cookies["foo"] = "cookie #$$"
+    render :text => ""
+  end
+
+  def xnotice
+    flash[:notice] = "session #$$"
+    render :text => ""
+  end
+
+  def xpost
+    if request.post?
+      digest = Digest::SHA1.new
+      out = "params: #{params.inspect}\n"
+      if file = params[:file]
+        loop do
+          buf = file.read(4096) or break
+          digest.update(buf)
+        end
+        out << "sha1: #{digest.to_s}\n"
+      end
+      headers['content-type'] = 'text/plain'
+      render :text => out
+    else
+      render :status => 403, :text => "need post\n"
+    end
+  end
+end
diff --git a/test/rails/app-2.2.2/app/helpers/application_helper.rb b/test/rails/app-2.2.2/app/helpers/application_helper.rb
new file mode 100644
index 0000000..de6be79
--- /dev/null
+++ b/test/rails/app-2.2.2/app/helpers/application_helper.rb
@@ -0,0 +1,2 @@
+module ApplicationHelper
+end
diff --git a/test/rails/app-2.2.2/config/boot.rb b/test/rails/app-2.2.2/config/boot.rb
new file mode 100644
index 0000000..0a51688
--- /dev/null
+++ b/test/rails/app-2.2.2/config/boot.rb
@@ -0,0 +1,109 @@
+# Don't change this file!
+# Configure your app in config/environment.rb and config/environments/*.rb
+
+RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
+
+module Rails
+  class << self
+    def boot!
+      unless booted?
+        preinitialize
+        pick_boot.run
+      end
+    end
+
+    def booted?
+      defined? Rails::Initializer
+    end
+
+    def pick_boot
+      (vendor_rails? ? VendorBoot : GemBoot).new
+    end
+
+    def vendor_rails?
+      File.exist?("#{RAILS_ROOT}/vendor/rails")
+    end
+
+    def preinitialize
+      load(preinitializer_path) if File.exist?(preinitializer_path)
+    end
+
+    def preinitializer_path
+      "#{RAILS_ROOT}/config/preinitializer.rb"
+    end
+  end
+
+  class Boot
+    def run
+      load_initializer
+      Rails::Initializer.run(:set_load_path)
+    end
+  end
+
+  class VendorBoot < Boot
+    def load_initializer
+      require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
+      Rails::Initializer.run(:install_gem_spec_stubs)
+    end
+  end
+
+  class GemBoot < Boot
+    def load_initializer
+      self.class.load_rubygems
+      load_rails_gem
+      require 'initializer'
+    end
+
+    def load_rails_gem
+      if version = self.class.gem_version
+        gem 'rails', version
+      else
+        gem 'rails'
+      end
+    rescue Gem::LoadError => load_error
+      $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.)
+      exit 1
+    end
+
+    class << self
+      def rubygems_version
+        Gem::RubyGemsVersion rescue nil
+      end
+
+      def gem_version
+        if defined? RAILS_GEM_VERSION
+          RAILS_GEM_VERSION
+        elsif ENV.include?('RAILS_GEM_VERSION')
+          ENV['RAILS_GEM_VERSION']
+        else
+          parse_gem_version(read_environment_rb)
+        end
+      end
+
+      def load_rubygems
+        require 'rubygems'
+        min_version = '1.3.1'
+        unless rubygems_version >= min_version
+          $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.)
+          exit 1
+        end
+
+      rescue LoadError
+        $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org)
+        exit 1
+      end
+
+      def parse_gem_version(text)
+        $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
+      end
+
+      private
+        def read_environment_rb
+          File.read("#{RAILS_ROOT}/config/environment.rb")
+        end
+    end
+  end
+end
+
+# All that for this:
+Rails.boot!
diff --git a/test/rails/app-2.2.2/config/database.yml b/test/rails/app-2.2.2/config/database.yml
new file mode 100644
index 0000000..9f77843
--- /dev/null
+++ b/test/rails/app-2.2.2/config/database.yml
@@ -0,0 +1,12 @@
+development:
+  adapter: sqlite3
+  database: db/development.sqlite3
+  timeout: 5000
+test:
+  adapter: sqlite3
+  database: db/test.sqlite3
+  timeout: 5000
+production:
+  adapter: sqlite3
+  database: db/production.sqlite3
+  timeout: 5000
diff --git a/test/rails/app-2.2.2/config/environment.rb b/test/rails/app-2.2.2/config/environment.rb
new file mode 100644
index 0000000..7c720f6
--- /dev/null
+++ b/test/rails/app-2.2.2/config/environment.rb
@@ -0,0 +1,15 @@
+unless defined? RAILS_GEM_VERSION
+  RAILS_GEM_VERSION = ENV['UNICORN_RAILS_VERSION']
+end
+
+# Bootstrap the Rails environment, frameworks, and default configuration
+require File.join(File.dirname(__FILE__), 'boot')
+
+Rails::Initializer.run do |config|
+  config.frameworks -= [ :action_web_service, :action_mailer ]
+  config.action_controller.session_store = :active_record_store
+  config.action_controller.session = {
+    :session_key => "_unicorn_rails_test.#{rand}",
+    :secret => "#{rand}#{rand}#{rand}#{rand}",
+  }
+end
diff --git a/test/rails/app-2.2.2/config/environments/development.rb b/test/rails/app-2.2.2/config/environments/development.rb
new file mode 100644
index 0000000..7f49032
--- /dev/null
+++ b/test/rails/app-2.2.2/config/environments/development.rb
@@ -0,0 +1,5 @@
+config.cache_classes = false
+config.whiny_nils = true
+config.action_controller.consider_all_requests_local = true
+config.action_controller.perform_caching = false
+config.action_view.debug_rjs = true
diff --git a/test/rails/app-2.2.2/config/environments/production.rb b/test/rails/app-2.2.2/config/environments/production.rb
new file mode 100644
index 0000000..c4059e3
--- /dev/null
+++ b/test/rails/app-2.2.2/config/environments/production.rb
@@ -0,0 +1,3 @@
+config.cache_classes = true
+config.action_controller.consider_all_requests_local = false
+config.action_controller.perform_caching             = true
diff --git a/test/rails/app-2.2.2/config/routes.rb b/test/rails/app-2.2.2/config/routes.rb
new file mode 100644
index 0000000..774028f
--- /dev/null
+++ b/test/rails/app-2.2.2/config/routes.rb
@@ -0,0 +1,4 @@
+ActionController::Routing::Routes.draw do |map|
+  map.connect ':controller/:action/:id.:format'
+  map.connect ':controller/:action/:id'
+end
diff --git a/test/rails/app-2.2.2/db/.gitignore b/test/rails/app-2.2.2/db/.gitignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/rails/app-2.2.2/db/.gitignore
diff --git a/test/rails/app-2.2.2/log/.gitignore b/test/rails/app-2.2.2/log/.gitignore
new file mode 100644
index 0000000..397b4a7
--- /dev/null
+++ b/test/rails/app-2.2.2/log/.gitignore
@@ -0,0 +1 @@
+*.log
diff --git a/test/rails/app-2.2.2/public/404.html b/test/rails/app-2.2.2/public/404.html
new file mode 100644
index 0000000..44d986c
--- /dev/null
+++ b/test/rails/app-2.2.2/public/404.html
@@ -0,0 +1 @@
+404 Not Found
diff --git a/test/rails/app-2.2.2/public/500.html b/test/rails/app-2.2.2/public/500.html
new file mode 100644
index 0000000..e534a49
--- /dev/null
+++ b/test/rails/app-2.2.2/public/500.html
@@ -0,0 +1 @@
+500 Internal Server Error
diff --git a/test/rails/app-2.3.2.1/.gitignore b/test/rails/app-2.3.2.1/.gitignore
new file mode 100644
index 0000000..f451f91
--- /dev/null
+++ b/test/rails/app-2.3.2.1/.gitignore
@@ -0,0 +1,2 @@
+/tmp
+/vendor
diff --git a/test/rails/app-2.3.2.1/Rakefile b/test/rails/app-2.3.2.1/Rakefile
new file mode 100644
index 0000000..fbebfca
--- /dev/null
+++ b/test/rails/app-2.3.2.1/Rakefile
@@ -0,0 +1,7 @@
+require(File.join(File.dirname(__FILE__), 'config', 'boot'))
+
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+require 'tasks/rails'
diff --git a/test/rails/app-2.3.2.1/app/controllers/application_controller.rb b/test/rails/app-2.3.2.1/app/controllers/application_controller.rb
new file mode 100644
index 0000000..6160f52
--- /dev/null
+++ b/test/rails/app-2.3.2.1/app/controllers/application_controller.rb
@@ -0,0 +1,3 @@
+class ApplicationController < ActionController::Base
+  helper :all
+end
diff --git a/test/rails/app-2.3.2.1/app/controllers/foo_controller.rb b/test/rails/app-2.3.2.1/app/controllers/foo_controller.rb
new file mode 100644
index 0000000..261669c
--- /dev/null
+++ b/test/rails/app-2.3.2.1/app/controllers/foo_controller.rb
@@ -0,0 +1,34 @@
+require 'digest/sha1'
+class FooController < ApplicationController
+  def index
+    render :text => "FOO\n"
+  end
+
+  def xcookie
+    cookies["foo"] = "cookie-#$$-#{session[:gotta_use_the_session_in_2_3]}"
+    render :text => ""
+  end
+
+  def xnotice
+    flash[:notice] = "session #$$"
+    render :text => ""
+  end
+
+  def xpost
+    if request.post?
+      digest = Digest::SHA1.new
+      out = "params: #{params.inspect}\n"
+      if file = params[:file]
+        loop do
+          buf = file.read(4096) or break
+          digest.update(buf)
+        end
+        out << "sha1: #{digest.to_s}\n"
+      end
+      headers['content-type'] = 'text/plain'
+      render :text => out
+    else
+      render :status => 403, :text => "need post\n"
+    end
+  end
+end
diff --git a/test/rails/app-2.3.2.1/app/helpers/application_helper.rb b/test/rails/app-2.3.2.1/app/helpers/application_helper.rb
new file mode 100644
index 0000000..de6be79
--- /dev/null
+++ b/test/rails/app-2.3.2.1/app/helpers/application_helper.rb
@@ -0,0 +1,2 @@
+module ApplicationHelper
+end
diff --git a/test/rails/app-2.3.2.1/config/boot.rb b/test/rails/app-2.3.2.1/config/boot.rb
new file mode 100644
index 0000000..d22e6b0
--- /dev/null
+++ b/test/rails/app-2.3.2.1/config/boot.rb
@@ -0,0 +1,107 @@
+RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
+
+module Rails
+  class << self
+    def boot!
+      unless booted?
+        preinitialize
+        pick_boot.run
+      end
+    end
+
+    def booted?
+      defined? Rails::Initializer
+    end
+
+    def pick_boot
+      (vendor_rails? ? VendorBoot : GemBoot).new
+    end
+
+    def vendor_rails?
+      File.exist?("#{RAILS_ROOT}/vendor/rails")
+    end
+
+    def preinitialize
+      load(preinitializer_path) if File.exist?(preinitializer_path)
+    end
+
+    def preinitializer_path
+      "#{RAILS_ROOT}/config/preinitializer.rb"
+    end
+  end
+
+  class Boot
+    def run
+      load_initializer
+      Rails::Initializer.run(:set_load_path)
+    end
+  end
+
+  class VendorBoot < Boot
+    def load_initializer
+      require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
+      Rails::Initializer.run(:install_gem_spec_stubs)
+      Rails::GemDependency.add_frozen_gem_path
+    end
+  end
+
+  class GemBoot < Boot
+    def load_initializer
+      self.class.load_rubygems
+      load_rails_gem
+      require 'initializer'
+    end
+
+    def load_rails_gem
+      if version = self.class.gem_version
+        gem 'rails', version
+      else
+        gem 'rails'
+      end
+    rescue Gem::LoadError => load_error
+      $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.)
+      exit 1
+    end
+
+    class << self
+      def rubygems_version
+        Gem::RubyGemsVersion rescue nil
+      end
+
+      def gem_version
+        if defined? RAILS_GEM_VERSION
+          RAILS_GEM_VERSION
+        elsif ENV.include?('RAILS_GEM_VERSION')
+          ENV['RAILS_GEM_VERSION']
+        else
+          parse_gem_version(read_environment_rb)
+        end
+      end
+
+      def load_rubygems
+        require 'rubygems'
+        min_version = '1.3.1'
+        unless rubygems_version >= min_version
+          $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.)
+          exit 1
+        end
+
+      rescue LoadError
+        $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org)
+        exit 1
+      end
+
+      def parse_gem_version(text)
+        $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
+      end
+
+      private
+        def read_environment_rb
+          File.read("#{RAILS_ROOT}/config/environment.rb")
+        end
+    end
+  end
+end
+
+# All that for this:
+Rails.boot!
diff --git a/test/rails/app-2.3.2.1/config/database.yml b/test/rails/app-2.3.2.1/config/database.yml
new file mode 100644
index 0000000..9f77843
--- /dev/null
+++ b/test/rails/app-2.3.2.1/config/database.yml
@@ -0,0 +1,12 @@
+development:
+  adapter: sqlite3
+  database: db/development.sqlite3
+  timeout: 5000
+test:
+  adapter: sqlite3
+  database: db/test.sqlite3
+  timeout: 5000
+production:
+  adapter: sqlite3
+  database: db/production.sqlite3
+  timeout: 5000
diff --git a/test/rails/app-2.3.2.1/config/environment.rb b/test/rails/app-2.3.2.1/config/environment.rb
new file mode 100644
index 0000000..17abdb7
--- /dev/null
+++ b/test/rails/app-2.3.2.1/config/environment.rb
@@ -0,0 +1,15 @@
+unless defined? RAILS_GEM_VERSION
+  RAILS_GEM_VERSION = ENV['UNICORN_RAILS_VERSION']
+end
+
+# Bootstrap the Rails environment, frameworks, and default configuration
+require File.join(File.dirname(__FILE__), 'boot')
+
+Rails::Initializer.run do |config|
+  config.frameworks -= [ :active_resource, :action_mailer ]
+  config.action_controller.session_store = :active_record_store
+  config.action_controller.session = {
+    :session_key => "_unicorn_rails_test.#{rand}",
+    :secret => "#{rand}#{rand}#{rand}#{rand}",
+  }
+end
diff --git a/test/rails/app-2.3.2.1/config/environments/development.rb b/test/rails/app-2.3.2.1/config/environments/development.rb
new file mode 100644
index 0000000..55376c5
--- /dev/null
+++ b/test/rails/app-2.3.2.1/config/environments/development.rb
@@ -0,0 +1,5 @@
+config.cache_classes = false
+config.whiny_nils = true
+config.action_controller.consider_all_requests_local = true
+config.action_view.debug_rjs                         = true
+config.action_controller.perform_caching             = false
diff --git a/test/rails/app-2.3.2.1/config/environments/production.rb b/test/rails/app-2.3.2.1/config/environments/production.rb
new file mode 100644
index 0000000..474257d
--- /dev/null
+++ b/test/rails/app-2.3.2.1/config/environments/production.rb
@@ -0,0 +1,4 @@
+config.cache_classes = true
+config.action_controller.consider_all_requests_local = false
+config.action_controller.perform_caching             = true
+config.action_view.cache_template_loading            = true
diff --git a/test/rails/app-2.3.2.1/config/routes.rb b/test/rails/app-2.3.2.1/config/routes.rb
new file mode 100644
index 0000000..4248853
--- /dev/null
+++ b/test/rails/app-2.3.2.1/config/routes.rb
@@ -0,0 +1,4 @@
+ActionController::Routing::Routes.draw do |map|
+  map.connect ':controller/:action/:id'
+  map.connect ':controller/:action/:id.:format'
+end
diff --git a/test/rails/app-2.3.2.1/db/.gitignore b/test/rails/app-2.3.2.1/db/.gitignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/rails/app-2.3.2.1/db/.gitignore
diff --git a/test/rails/app-2.3.2.1/log/.gitignore b/test/rails/app-2.3.2.1/log/.gitignore
new file mode 100644
index 0000000..397b4a7
--- /dev/null
+++ b/test/rails/app-2.3.2.1/log/.gitignore
@@ -0,0 +1 @@
+*.log
diff --git a/test/rails/app-2.3.2.1/public/404.html b/test/rails/app-2.3.2.1/public/404.html
new file mode 100644
index 0000000..44d986c
--- /dev/null
+++ b/test/rails/app-2.3.2.1/public/404.html
@@ -0,0 +1 @@
+404 Not Found
diff --git a/test/rails/app-2.3.2.1/public/500.html b/test/rails/app-2.3.2.1/public/500.html
new file mode 100644
index 0000000..e534a49
--- /dev/null
+++ b/test/rails/app-2.3.2.1/public/500.html
@@ -0,0 +1 @@
+500 Internal Server Error
diff --git a/test/rails/test_rails.rb b/test/rails/test_rails.rb
new file mode 100644
index 0000000..c7add20
--- /dev/null
+++ b/test/rails/test_rails.rb
@@ -0,0 +1,247 @@
+# Copyright (c) 2009 Eric Wong
+require 'test/test_helper'
+
+# don't call exit(0) since it may be run under rake (but gmake is recommended)
+do_test = true
+
+$unicorn_rails_bin = ENV['UNICORN_RAILS_TEST_BIN'] || "unicorn_rails"
+redirect_test_io { do_test = system($unicorn_rails_bin, '-v') }
+
+unless do_test
+  warn "#$unicorn_rails_bin not found in PATH=#{ENV['PATH']}, " \
+       "skipping this test"
+end
+
+unless which('git')
+  warn "git not found in PATH=#{ENV['PATH']}, skipping this test"
+  do_test = false
+end
+
+if RAILS_GIT_REPO = ENV['RAILS_GIT_REPO']
+  unless File.directory?(RAILS_GIT_REPO)
+    warn "#{RAILS_GIT_REPO} not found, create it with:\n" \
+         "\tgit clone --mirror git://github.com/rails/rails #{RAILS_GIT_REPO}" \
+         "skipping this test for now"
+    do_test = false
+  end
+else
+  warn "RAILS_GIT_REPO not defined, don't know where to git clone from"
+  do_test = false
+end
+
+unless UNICORN_RAILS_TEST_VERSION = ENV['UNICORN_RAILS_TEST_VERSION']
+  warn 'UNICORN_RAILS_TEST_VERSION not defined in environment, ' \
+       'skipping this test'
+  do_test = false
+end
+
+RAILS_ROOT = "#{File.dirname(__FILE__)}/app-#{UNICORN_RAILS_TEST_VERSION}"
+unless File.directory?(RAILS_ROOT)
+  warn "unsupported UNICORN_RAILS_TEST_VERSION=#{UNICORN_RAILS_TEST_VERSION}"
+  do_test = false
+end
+
+ROR_V = UNICORN_RAILS_TEST_VERSION.split(/\./).map { |x| x.to_i }
+RB_V = RUBY_VERSION.split(/\./).map { |x| x.to_i }
+if RB_V[0] >= 1 && RB_V[1] >= 9
+  unless ROR_V[0] >= 2 && ROR_V[1] >= 3
+    warn "skipping Ruby >=1.9 test with Rails <2.3"
+    do_test = false
+  end
+end
+
+class RailsTest < Test::Unit::TestCase
+  trap(:QUIT, 'IGNORE')
+
+  COMMON_TMP = Tempfile.new('unicorn_tmp') unless defined?(COMMON_TMP)
+
+  HEAVY_CFG = <<-EOS
+worker_processes 2
+timeout 30
+logger Logger.new('#{COMMON_TMP.path}')
+  EOS
+
+  def setup
+    @pwd = Dir.pwd
+    @tmpfile = Tempfile.new('unicorn_rails_test')
+    @tmpdir = @tmpfile.path
+    @tmpfile.close!
+    assert_nothing_raised do
+      FileUtils.cp_r(RAILS_ROOT, @tmpdir, :preserve => true)
+    end
+    Dir.chdir(@tmpdir)
+    system('git', 'clone', '-nsq', RAILS_GIT_REPO, 'vendor/rails')
+    Dir.chdir("#@tmpdir/vendor/rails") do
+      system('git', 'reset', '-q', '--hard', "v#{UNICORN_RAILS_TEST_VERSION}")
+    end
+
+    assert(system('rake', 'db:sessions:create'))
+    assert(system('rake', 'db:migrate'))
+
+    @addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
+    @port = unused_port(@addr)
+    @start_pid = $$
+    @pid = nil
+  end
+
+  def test_launcher
+    tmp_dirs = %w(cache pids sessions sockets)
+    tmp_dirs.each { |dir| assert(! File.exist?("tmp/#{dir}")) }
+    redirect_test_io { @pid = fork { exec 'unicorn_rails', "-l#@addr:#@port" } }
+    wait_master_ready("test_stderr.#$$.log")
+
+    # temp dirs exist
+    tmp_dirs.each { |dir| assert(File.directory?("tmp/#{dir}")) }
+
+    # basic GET
+    res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/foo"))
+    assert_equal "FOO\n", res.body
+    assert_match %r{^text/html\b}, res['Content-Type']
+    assert_equal "4", res['Content-Length']
+    assert_equal "200 OK", res['Status']
+
+    # can we set cookies?
+    res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/foo/xcookie"))
+    assert_equal "200", res.code
+    assert_equal "200 OK", res['Status']
+    cookies = res.get_fields('Set-Cookie')
+    assert_equal 2, cookies.size
+    assert_equal 1, cookies.grep(/\A_unicorn_rails_test\./).size
+    assert_equal 1, cookies.grep(/\Afoo=cookie/).size
+
+    # how about just a session?
+    res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/foo/xnotice"))
+    assert_equal "200", res.code
+    assert_equal "200 OK", res['Status']
+    cookies = res.get_fields('Set-Cookie')
+    assert_equal 1, cookies.size
+    assert_equal 1, cookies.grep(/\A_unicorn_rails_test\./).size
+
+    # posting forms?
+    uri = URI.parse("http://#@addr:#@port/foo/xpost")
+    wait_master_ready("test_stderr.#$$.log")
+    res = Net::HTTP.post_form(uri, {"a" => "b", "c"=>"d"})
+    assert_equal "200", res.code
+    params = res.body.split(/\n/).grep(/^params:/)
+    assert_equal 1, params.size
+    params = eval(params[0].gsub!(/\Aparams:/, ''))
+    assert_equal Hash, params.class
+    assert_equal 'b', params['a']
+    assert_equal 'd', params['c']
+    assert_equal "200 OK", res['Status']
+
+    # try uploading a big file
+    tmp = Tempfile.new('random')
+    sha1 = Digest::SHA1.new
+    assert_nothing_raised do
+      File.open("/dev/urandom", "rb") do |fp|
+        256.times do
+          buf = fp.sysread(4096)
+          sha1.update(buf)
+          tmp.syswrite(buf)
+        end
+      end
+    end
+    resp = `curl -isSfN -Ffile=@#{tmp.path} http://#@addr:#@port/foo/xpost`
+    assert $?.success?
+    resp = resp.split(/\r?\n/)
+    grepped = resp.grep(/^sha1: (.{40})/)
+    assert_equal 1, grepped.size
+    assert_equal(sha1.hexdigest, /^sha1: (.{40})/.match(grepped.first)[1])
+
+    grepped = resp.grep(/^Content-Type:\s+(.+)/i)
+    assert_equal 1, grepped.size
+    assert_match %r{^text/plain}, grepped.first.split(/\s*:\s*/)[1]
+
+    assert_equal 1, resp.grep(/^Status:/i).size
+
+    # make sure we can get 403 responses, too
+    uri = URI.parse("http://#@addr:#@port/foo/xpost")
+    wait_master_ready("test_stderr.#$$.log")
+    res = Net::HTTP.get_response(uri)
+    assert_equal "403", res.code
+    assert_equal "403 Forbidden", res['Status']
+
+    # non existent controller
+    uri = URI.parse("http://#@addr:#@port/asdf")
+    res = Net::HTTP.get_response(uri)
+    assert_equal "404", res.code
+    assert_equal "404 Not Found", res['Status']
+
+    # static files
+
+    # ensure file we're about to serve is not there yet
+    res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/pid.txt"))
+    assert_equal "404 Not Found", res['Status']
+    assert_equal '404', res.code
+
+    # can we serve text files based on suffix?
+    File.open("public/pid.txt", "wb") { |fp| fp.syswrite("#$$\n") }
+    res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/pid.txt"))
+    assert_equal '200', res.code
+    assert_equal "200 OK", res['Status']
+    assert_match %r{^text/plain}, res['Content-Type']
+    assert_equal "#$$\n", res.body
+
+    # can we serve HTML files based on suffix?
+    assert File.exist?("public/500.html")
+    res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/500.html"))
+    assert_equal '200', res.code
+    assert_equal '200 OK', res['Status']
+    assert_match %r{^text/html}, res['Content-Type']
+    five_hundred_body = res.body
+
+    # lets try pretending 500 is a controller that got cached
+    assert ! File.exist?("public/500")
+    assert_equal five_hundred_body, File.read("public/500.html")
+    res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/500"))
+    assert_equal '200', res.code
+    assert_equal '200 OK', res['Status']
+    assert_match %r{^text/html}, res['Content-Type']
+    assert_equal five_hundred_body, res.body
+  end
+
+  def test_alt_url_root
+    # cbf to actually work on this since I never use this feature (ewong)
+    return unless ROR_V[0] >= 2 && ROR_V[1] >= 3
+    redirect_test_io do
+      @pid = fork { exec 'unicorn_rails', "-l#@addr:#@port", '-P/poo' }
+    end
+    wait_master_ready("test_stderr.#$$.log")
+    res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/poo/foo"))
+    # p res
+    # p res.body
+    # system 'cat', 'log/development.log'
+    assert_equal "200", res.code
+    assert_equal '200 OK', res['Status']
+    assert_equal "FOO\n", res.body
+    assert_match %r{^text/html\b}, res['Content-Type']
+    assert_equal "4", res['Content-Length']
+
+    res = Net::HTTP.get_response(URI.parse("http://#@addr:#@port/foo"))
+    assert_equal "404", res.code
+    assert_equal '404 Not Found', res['Status']
+  end
+
+  def teardown
+    return if @start_pid != $$
+
+    if @pid
+      Process.kill(:QUIT, @pid)
+      pid2, status = Process.waitpid2(@pid)
+      assert status.success?
+    end
+
+    Dir.chdir(@pwd)
+    FileUtils.rmtree(@tmpdir)
+    loop do
+      Process.kill('-QUIT', 0)
+      begin
+        Process.waitpid(-1, Process::WNOHANG) or break
+      rescue Errno::ECHILD
+        break
+      end
+    end
+  end
+
+end if do_test
diff --git a/test/test_helper.rb b/test/test_helper.rb
index f809af3..55aa70c 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -4,9 +4,16 @@
 # Additional work donated by contributors.  See http://mongrel.rubyforge.org/attributions.html
 # for more information.
 
+STDIN.sync = STDOUT.sync = STDERR.sync = true # buffering makes debugging hard
+
+# Some tests watch a log file or a pid file to spring up to check state
+# Can't rely on inotify on non-Linux and logging to a pipe makes things
+# more complicated
+DEFAULT_TRIES = 1000
+DEFAULT_RES = 0.2
 
 HERE = File.dirname(__FILE__) unless defined?(HERE)
-%w(lib ext bin test).each do |dir|
+%w(lib ext).each do |dir|
   $LOAD_PATH.unshift "#{HERE}/../#{dir}"
 end
 
@@ -15,8 +22,10 @@ require 'net/http'
 require 'digest/sha1'
 require 'uri'
 require 'stringio'
+require 'pathname'
+require 'tempfile'
+require 'fileutils'
 require 'unicorn'
-require 'tmpdir'
 
 if ENV['DEBUG']
   require 'ruby-debug'
@@ -26,8 +35,9 @@ end
 def redirect_test_io
   orig_err = STDERR.dup
   orig_out = STDOUT.dup
-  STDERR.reopen("test_stderr.#{$$}.log")
-  STDOUT.reopen("test_stdout.#{$$}.log")
+  STDERR.reopen("test_stderr.#{$$}.log", "a")
+  STDOUT.reopen("test_stdout.#{$$}.log", "a")
+  STDERR.sync = STDOUT.sync = true
 
   at_exit do
     File.unlink("test_stderr.#{$$}.log") rescue nil
@@ -41,7 +51,18 @@ def redirect_test_io
     STDOUT.reopen(orig_out)
   end
 end
-    
+
+# which(1) exit codes cannot be trusted on some systems
+# We use UNIX shell utilities in some tests because we don't trust
+# ourselves to write Ruby 100% correctly :)
+def which(bin)
+  ex = ENV['PATH'].split(/:/).detect do |x|
+    x << "/#{bin}"
+    File.executable?(x)
+  end or warn "`#{bin}' not found in PATH=#{ENV['PATH']}"
+  ex
+end
+
 # Either takes a string to do a get request against, or a tuple of [URI, HTTP] where
 # HTTP is some kind of Net::HTTP request object (POST, HEAD, etc.)
 def hit(uris)
@@ -101,3 +122,141 @@ def unused_port(addr = '127.0.0.1')
   sock.close rescue nil
   port
 end
+
+def try_require(lib)
+  begin
+    require lib
+    true
+  rescue LoadError
+    false
+  end
+end
+
+# sometimes the server may not come up right away
+def retry_hit(uris = [])
+  tries = DEFAULT_TRIES
+  begin
+    hit(uris)
+  rescue Errno::ECONNREFUSED => err
+    if (tries -= 1) > 0
+      sleep DEFAULT_RES
+      retry
+    end
+    raise err
+  end
+end
+
+def assert_shutdown(pid)
+  wait_master_ready("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_workers_ready(path, nr_workers)
+  tries = DEFAULT_TRIES
+  lines = []
+  while (tries -= 1) > 0
+    begin
+      lines = File.readlines(path).grep(/worker=\d+ ready/)
+      lines.size == nr_workers and return
+    rescue Errno::ENOENT
+    end
+    sleep DEFAULT_RES
+  end
+  raise "#{nr_workers} workers never became ready:" \
+        "\n\t#{lines.join("\n\t")}\n"
+end
+
+def wait_master_ready(master_log)
+  tries = DEFAULT_TRIES
+  while (tries -= 1) > 0
+    begin
+      File.readlines(master_log).grep(/master process ready/)[0] and return
+    rescue Errno::ENOENT
+    end
+    sleep DEFAULT_RES
+  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)
+
+  old_pid = File.read("#{pid_file}.oldbin").to_i
+  new_pid = File.read(pid_file).to_i
+
+  # kill old master process
+  assert_not_equal pid, new_pid
+  assert_equal pid, old_pid
+  assert_nothing_raised { Process.kill(:QUIT, old_pid) }
+  assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
+  wait_for_death(old_pid)
+  assert_equal new_pid, File.read(pid_file).to_i
+  assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) }
+  assert_nothing_raised { Process.kill(:QUIT, new_pid) }
+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 = DEFAULT_TRIES
+  while (tries -= 1) > 0 && ! File.exist?(path)
+    sleep DEFAULT_RES
+  end
+  assert File.exist?(path), "path=#{path} exists #{caller.inspect}"
+end
+
+def xfork(&block)
+  fork do
+    ObjectSpace.each_object(Tempfile) do |tmp|
+      ObjectSpace.undefine_finalizer(tmp)
+    end
+    yield
+  end
+end
+
+# can't waitpid on detached processes
+def wait_for_death(pid)
+  tries = DEFAULT_TRIES
+  while (tries -= 1) > 0
+    begin
+      Process.kill(0, pid)
+      begin
+        Process.waitpid(pid, Process::WNOHANG)
+      rescue Errno::ECHILD
+      end
+      sleep(DEFAULT_RES)
+    rescue Errno::ESRCH
+      return
+    end
+  end
+  raise "PID:#{pid} never died!"
+end
diff --git a/test/tools/trickletest.rb b/test/tools/trickletest.rb
deleted file mode 100644
index e19ed71..0000000
--- a/test/tools/trickletest.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-require 'socket'
-require 'stringio'
-
-def do_test(st, chunk)
-  s = TCPSocket.new('127.0.0.1',ARGV[0].to_i);
-  req = StringIO.new(st)
-  nout = 0
-  randstop = rand(st.length / 10)
-  STDERR.puts "stopping after: #{randstop}"
-
-  begin
-    while data = req.read(chunk)
-      nout += s.write(data)
-      s.flush
-      sleep 0.1
-      if nout > randstop
-        STDERR.puts "BANG! after #{nout} bytes."
-        break
-      end
-    end
-  rescue Object => e
-    STDERR.puts "ERROR: #{e}"
-  ensure
-    s.close
-  end
-end
-
-content = "-" * (1024 * 240)
-st = "GET / HTTP/1.1\r\nHost: www.zedshaw.com\r\nContent-Type: text/plain\r\nContent-Length: #{content.length}\r\n\r\n#{content}"
-
-puts "length: #{content.length}"
-
-threads = []
-ARGV[1].to_i.times do
-  t = Thread.new do
-    size = 100
-    puts ">>>> #{size} sized chunks"
-    do_test(st, size)
-  end
-
-  t.abort_on_exception = true
-  threads << t
-end
-
-threads.each {|t|  t.join}
diff --git a/test/unit/test_configurator.rb b/test/unit/test_configurator.rb
index 8de0b13..98f2db6 100644
--- a/test/unit/test_configurator.rb
+++ b/test/unit/test_configurator.rb
@@ -4,10 +4,34 @@ require 'unicorn/configurator'
 
 class TestConfigurator < Test::Unit::TestCase
 
-  def test_config_defaults
+  def test_config_init
     assert_nothing_raised { Unicorn::Configurator.new {} }
   end
 
+  def test_expand_addr
+    meth = Unicorn::Configurator.new.method(:expand_addr)
+
+    assert_equal "/var/run/unicorn.sock", meth.call("/var/run/unicorn.sock")
+    assert_equal "#{Dir.pwd}/foo/bar.sock", meth.call("unix:foo/bar.sock")
+
+    path = meth.call("~/foo/bar.sock")
+    assert_equal "/", path[0..0]
+    assert_match %r{/foo/bar\.sock\z}, path
+
+    path = meth.call("~root/foo/bar.sock")
+    assert_equal "/", path[0..0]
+    assert_match %r{/foo/bar\.sock\z}, path
+
+    assert_equal "1.2.3.4:2007", meth.call('1.2.3.4:2007')
+    assert_equal "0.0.0.0:2007", meth.call('0.0.0.0:2007')
+    assert_equal "0.0.0.0:2007", meth.call(':2007')
+    assert_equal "0.0.0.0:2007", meth.call('*:2007')
+    assert_equal "0.0.0.0:2007", meth.call('2007')
+    assert_equal "0.0.0.0:2007", meth.call(2007)
+    assert_match %r{\A\d+\.\d+\.\d+\.\d+:2007\z}, meth.call('1:2007')
+    assert_match %r{\A\d+\.\d+\.\d+\.\d+:2007\z}, meth.call('2:2007')
+  end
+
   def test_config_invalid
     tmp = Tempfile.new('unicorn_config')
     tmp.syswrite(%q(asdfasdf "hello-world"))
@@ -45,4 +69,43 @@ class TestConfigurator < Test::Unit::TestCase
     assert_nil @logger
   end
 
+  def test_listen_options
+    tmp = Tempfile.new('unicorn_config')
+    expect = { :sndbuf => 1, :rcvbuf => 2, :backlog => 10 }.freeze
+    listener = "127.0.0.1:12345"
+    tmp.syswrite("listen '#{listener}', #{expect.inspect}\n")
+    cfg = nil
+    assert_nothing_raised do
+      cfg = Unicorn::Configurator.new(:config_file => tmp.path)
+    end
+    assert_nothing_raised { cfg.commit!(self) }
+    assert(listener_opts = instance_variable_get("@listener_opts"))
+    assert_equal expect, listener_opts[listener]
+  end
+
+  def test_listen_option_bad
+    tmp = Tempfile.new('unicorn_config')
+    expect = { :sndbuf => "five" }
+    listener = "127.0.0.1:12345"
+    tmp.syswrite("listen '#{listener}', #{expect.inspect}\n")
+    assert_raises(ArgumentError) do
+      Unicorn::Configurator.new(:config_file => tmp.path)
+    end
+  end
+
+  def test_after_fork_proc
+    [ proc { |a,b| }, Proc.new { |a,b| }, lambda { |a,b| } ].each do |my_proc|
+      Unicorn::Configurator.new(:after_fork => my_proc).commit!(self)
+      assert_equal my_proc, @after_fork
+    end
+  end
+
+  def test_after_fork_wrong_arity
+    [ proc { |a| }, Proc.new { }, lambda { |a,b,c| } ].each do |my_proc|
+      assert_raises(ArgumentError) do
+        Unicorn::Configurator.new(:after_fork => my_proc)
+      end
+    end
+  end
+
 end
diff --git a/test/unit/test_http_parser.rb b/test/unit/test_http_parser.rb
index ca1cd01..a158ebb 100644
--- a/test/unit/test_http_parser.rb
+++ b/test/unit/test_http_parser.rb
@@ -14,46 +14,82 @@ class HttpParserTest < Test::Unit::TestCase
     parser = HttpParser.new
     req = {}
     http = "GET / HTTP/1.1\r\n\r\n"
-    nread = parser.execute(req, http, 0)
-
-    assert nread == http.length, "Failed to parse the full HTTP request"
-    assert parser.finished?, "Parser didn't finish"
-    assert !parser.error?, "Parser had error"
-    assert nread == parser.nread, "Number read returned from execute does not match"
+    assert parser.execute(req, http)
 
     assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
     assert_equal '/', req['REQUEST_PATH']
     assert_equal 'HTTP/1.1', req['HTTP_VERSION']
     assert_equal '/', req['REQUEST_URI']
-    assert_equal 'CGI/1.2', req['GATEWAY_INTERFACE']
-    assert_equal 'GET', req['REQUEST_METHOD']    
+    assert_equal 'GET', req['REQUEST_METHOD']
     assert_nil req['FRAGMENT']
-    assert_nil req['QUERY_STRING']
-    
+    assert_equal '', req['QUERY_STRING']
+
     parser.reset
-    assert parser.nread == 0, "Number read after reset should be 0"
+    req.clear
+
+    assert ! parser.execute(req, "G")
+    assert req.empty?
+
+    # try parsing again to ensure we were reset correctly
+    http = "GET /hello-world HTTP/1.1\r\n\r\n"
+    assert parser.execute(req, http)
+
+    assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
+    assert_equal '/hello-world', req['REQUEST_PATH']
+    assert_equal 'HTTP/1.1', req['HTTP_VERSION']
+    assert_equal '/hello-world', req['REQUEST_URI']
+    assert_equal 'GET', req['REQUEST_METHOD']
+    assert_nil req['FRAGMENT']
+    assert_equal '', req['QUERY_STRING']
+  end
+
+  def test_parse_server_host_default_port
+    parser = HttpParser.new
+    req = {}
+    assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo\r\n\r\n")
+    assert_equal 'foo', req['SERVER_NAME']
+    assert_equal '80', req['SERVER_PORT']
+  end
+
+  def test_parse_server_host_alt_port
+    parser = HttpParser.new
+    req = {}
+    assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:999\r\n\r\n")
+    assert_equal 'foo', req['SERVER_NAME']
+    assert_equal '999', req['SERVER_PORT']
+  end
+
+  def test_parse_server_host_empty_port
+    parser = HttpParser.new
+    req = {}
+    assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:\r\n\r\n")
+    assert_equal 'foo', req['SERVER_NAME']
+    assert_equal '80', req['SERVER_PORT']
   end
-
+
+  def test_parse_server_host_xfp_https
+    parser = HttpParser.new
+    req = {}
+    assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:\r\n" \
+                          "X-Forwarded-Proto: https\r\n\r\n")
+    assert_equal 'foo', req['SERVER_NAME']
+    assert_equal '443', req['SERVER_PORT']
+  end
+
   def test_parse_strange_headers
     parser = HttpParser.new
     req = {}
     should_be_good = "GET / HTTP/1.1\r\naaaaaaaaaaaaa:++++++++++\r\n\r\n"
-    nread = parser.execute(req, should_be_good, 0)
-    assert_equal should_be_good.length, nread
-    assert parser.finished?
-    assert !parser.error?
+    assert parser.execute(req, should_be_good)
 
-    # ref: http://thread.gmane.org/gmane.comp.lang.ruby.Unicorn.devel/37/focus=45
+    # ref: http://thread.gmane.org/gmane.comp.lang.ruby.mongrel.devel/37/focus=45
     # (note we got 'pen' mixed up with 'pound' in that thread,
     # but the gist of it is still relevant: these nasty headers are irrelevant
     #
     # nasty_pound_header = "GET / HTTP/1.1\r\nX-SSL-Bullshit:   -----BEGIN CERTIFICATE-----\r\n\tMIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx\r\n\tETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT\r\n\tAkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu\r\n\tdWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV\r\n\tSzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV\r\n\tBAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB\r\n\tBQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF\r\n\tW51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR\r\n\tgW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL\r\n\t0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP\r\n\tu2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR\r\n\twgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG\r\n\tA1UdEwEB/wQCMAAwEQYJYIZIAYb4QgEBBAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs\r\n\tBglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD\r\n\tVR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj\r\n\tloCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj\r\n\taWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG\r\n\t9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE\r\n\tIjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO\r\n\tBgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1\r\n\tcHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4QgEDBDAWLmh0\r\n\tdHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC5jcmwwPwYD\r\n\tVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv\r\n\tY3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3\r\n\tXCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8\r\n\tUO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk\r\n\thTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK\r\n\twTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu\r\n\tYhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3\r\n\tRA==\r\n\t-----END CERTIFICATE-----\r\n\r\n"
     # parser = HttpParser.new
     # req = {}
-    # nread = parser.execute(req, nasty_pound_header, 0)
-    # assert_equal nasty_pound_header.length, nread
-    # assert parser.finished?
-    # assert !parser.error?
+    # assert parser.execute(req, nasty_pound_header, 0)
   end
 
   def test_parse_ie6_urls
@@ -67,10 +103,7 @@ class HttpParserTest < Test::Unit::TestCase
       parser = HttpParser.new
       req = {}
       sorta_safe = %(GET #{path} HTTP/1.1\r\n\r\n)
-      nread = parser.execute(req, sorta_safe, 0)
-      assert_equal sorta_safe.length, nread
-      assert parser.finished?
-      assert !parser.error?
+      assert parser.execute(req, sorta_safe)
     end
   end
   
@@ -79,28 +112,149 @@ class HttpParserTest < Test::Unit::TestCase
     req = {}
     bad_http = "GET / SsUTF/1.1"
 
-    error = false
-    begin
-      nread = parser.execute(req, bad_http, 0)
-    rescue => details
-      error = true
-    end
+    assert_raises(HttpParserError) { parser.execute(req, bad_http) }
+    parser.reset
+    assert(parser.execute({}, "GET / HTTP/1.0\r\n\r\n"))
+  end
+
+  def test_piecemeal
+    parser = HttpParser.new
+    req = {}
+    http = "GET"
+    assert ! parser.execute(req, http)
+    assert_raises(HttpParserError) { parser.execute(req, http) }
+    assert ! parser.execute(req, http << " / HTTP/1.0")
+    assert_equal '/', req['REQUEST_PATH']
+    assert_equal '/', req['REQUEST_URI']
+    assert_equal 'GET', req['REQUEST_METHOD']
+    assert ! parser.execute(req, http << "\r\n")
+    assert_equal 'HTTP/1.0', req['HTTP_VERSION']
+    assert ! parser.execute(req, http << "\r")
+    assert parser.execute(req, http << "\n")
+    assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
+    assert_nil req['FRAGMENT']
+    assert_equal '', req['QUERY_STRING']
+  end
+
+  # not common, but underscores do appear in practice
+  def test_absolute_uri_underscores
+    parser = HttpParser.new
+    req = {}
+    http = "GET http://under_score.example.com/foo?q=bar HTTP/1.0\r\n\r\n"
+    assert parser.execute(req, http)
+    assert_equal 'http', req['rack.url_scheme']
+    assert_equal '/foo?q=bar', req['REQUEST_URI']
+    assert_equal '/foo', req['REQUEST_PATH']
+    assert_equal 'q=bar', req['QUERY_STRING']
+
+    assert_equal 'under_score.example.com', req['HTTP_HOST']
+    assert_equal 'under_score.example.com', req['SERVER_NAME']
+    assert_equal '80', req['SERVER_PORT']
+  end
+
+  def test_absolute_uri
+    parser = HttpParser.new
+    req = {}
+    http = "GET http://example.com/foo?q=bar HTTP/1.0\r\n\r\n"
+    assert parser.execute(req, http)
+    assert_equal 'http', req['rack.url_scheme']
+    assert_equal '/foo?q=bar', req['REQUEST_URI']
+    assert_equal '/foo', req['REQUEST_PATH']
+    assert_equal 'q=bar', req['QUERY_STRING']
+
+    assert_equal 'example.com', req['HTTP_HOST']
+    assert_equal 'example.com', req['SERVER_NAME']
+    assert_equal '80', req['SERVER_PORT']
+  end
+
+  # X-Forwarded-Proto is not in rfc2616, absolute URIs are, however...
+  def test_absolute_uri_https
+    parser = HttpParser.new
+    req = {}
+    http = "GET https://example.com/foo?q=bar HTTP/1.1\r\n" \
+           "X-Forwarded-Proto: http\r\n\r\n"
+    assert parser.execute(req, http)
+    assert_equal 'https', req['rack.url_scheme']
+    assert_equal '/foo?q=bar', req['REQUEST_URI']
+    assert_equal '/foo', req['REQUEST_PATH']
+    assert_equal 'q=bar', req['QUERY_STRING']
+
+    assert_equal 'example.com', req['HTTP_HOST']
+    assert_equal 'example.com', req['SERVER_NAME']
+    assert_equal '443', req['SERVER_PORT']
+  end
+
+  # Host: header should be ignored for absolute URIs
+  def test_absolute_uri_with_port
+    parser = HttpParser.new
+    req = {}
+    http = "GET http://example.com:8080/foo?q=bar HTTP/1.2\r\n" \
+           "Host: bad.example.com\r\n\r\n"
+    assert parser.execute(req, http)
+    assert_equal 'http', req['rack.url_scheme']
+    assert_equal '/foo?q=bar', req['REQUEST_URI']
+    assert_equal '/foo', req['REQUEST_PATH']
+    assert_equal 'q=bar', req['QUERY_STRING']
+
+    assert_equal 'example.com:8080', req['HTTP_HOST']
+    assert_equal 'example.com', req['SERVER_NAME']
+    assert_equal '8080', req['SERVER_PORT']
+  end
+
+  def test_absolute_uri_with_empty_port
+    parser = HttpParser.new
+    req = {}
+    http = "GET https://example.com:/foo?q=bar HTTP/1.1\r\n" \
+           "Host: bad.example.com\r\n\r\n"
+    assert parser.execute(req, http)
+    assert_equal 'https', req['rack.url_scheme']
+    assert_equal '/foo?q=bar', req['REQUEST_URI']
+    assert_equal '/foo', req['REQUEST_PATH']
+    assert_equal 'q=bar', req['QUERY_STRING']
+
+    assert_equal 'example.com:', req['HTTP_HOST']
+    assert_equal 'example.com', req['SERVER_NAME']
+    assert_equal '443', req['SERVER_PORT']
+  end
+
+  def test_put_body_oneshot
+    parser = HttpParser.new
+    req = {}
+    http = "PUT / HTTP/1.0\r\nContent-Length: 5\r\n\r\nabcde"
+    assert parser.execute(req, http)
+    assert_equal '/', req['REQUEST_PATH']
+    assert_equal '/', req['REQUEST_URI']
+    assert_equal 'PUT', req['REQUEST_METHOD']
+    assert_equal 'HTTP/1.0', req['HTTP_VERSION']
+    assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
+    assert_equal "abcde", req[:http_body]
+  end
 
-    assert error, "failed to throw exception"
-    assert !parser.finished?, "Parser shouldn't be finished"
-    assert parser.error?, "Parser SHOULD have error"
+  def test_put_body_later
+    parser = HttpParser.new
+    req = {}
+    http = "PUT /l HTTP/1.0\r\nContent-Length: 5\r\n\r\n"
+    assert parser.execute(req, http)
+    assert_equal '/l', req['REQUEST_PATH']
+    assert_equal '/l', req['REQUEST_URI']
+    assert_equal 'PUT', req['REQUEST_METHOD']
+    assert_equal 'HTTP/1.0', req['HTTP_VERSION']
+    assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
+    assert_equal "", req[:http_body]
   end
 
   def test_fragment_in_uri
     parser = HttpParser.new
     req = {}
     get = "GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n"
+    ok = false
     assert_nothing_raised do
-      parser.execute(req, get, 0)
+      ok = parser.execute(req, get)
     end
-    assert parser.finished?
+    assert ok
     assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI']
     assert_equal 'posts-17408', req['FRAGMENT']
+    assert_equal 'page=1', req['QUERY_STRING']
   end
 
   # lame random garbage maker
@@ -125,7 +279,7 @@ class HttpParserTest < Test::Unit::TestCase
     10.times do |c|
       get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-#{rand_data(1024, 1024+(c*1024))}: Test\r\n\r\n"
       assert_raises Unicorn::HttpParserError do
-        parser.execute({}, get, 0)
+        parser.execute({}, get)
         parser.reset
       end
     end
@@ -134,7 +288,7 @@ class HttpParserTest < Test::Unit::TestCase
     10.times do |c|
       get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-Test: #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n"
       assert_raises Unicorn::HttpParserError do
-        parser.execute({}, get, 0)
+        parser.execute({}, get)
         parser.reset
       end
     end
@@ -143,7 +297,7 @@ class HttpParserTest < Test::Unit::TestCase
     get = "GET /#{rand_data(10,120)} HTTP/1.1\r\n"
     get << "X-Test: test\r\n" * (80 * 1024)
     assert_raises Unicorn::HttpParserError do
-      parser.execute({}, get, 0)
+      parser.execute({}, get)
       parser.reset
     end
 
@@ -151,7 +305,7 @@ class HttpParserTest < Test::Unit::TestCase
     10.times do |c|
       get = "GET #{rand_data(1024, 1024+(c*1024), false)} #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n"
       assert_raises Unicorn::HttpParserError do
-        parser.execute({}, get, 0)
+        parser.execute({}, get)
         parser.reset
       end
     end
diff --git a/test/unit/test_request.rb b/test/unit/test_request.rb
new file mode 100644
index 0000000..0bfff7d
--- /dev/null
+++ b/test/unit/test_request.rb
@@ -0,0 +1,159 @@
+# Copyright (c) 2009 Eric Wong
+# You can redistribute it and/or modify it under the same terms as Ruby.
+
+require 'test/test_helper'
+begin
+  require 'rack'
+  require 'rack/lint'
+rescue LoadError
+  warn "Unable to load rack, skipping test"
+  exit 0
+end
+
+include Unicorn
+
+class RequestTest < Test::Unit::TestCase
+
+  class MockRequest < StringIO
+    alias_method :readpartial, :sysread
+  end
+
+  def setup
+    @request = HttpRequest.new(Logger.new($stderr))
+    @app = lambda do |env|
+      [ 200, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ]
+    end
+    @lint = Rack::Lint.new(@app)
+  end
+
+  def test_options
+    client = MockRequest.new("OPTIONS * HTTP/1.1\r\n" \
+                             "Host: foo\r\n\r\n")
+    res = env = nil
+    assert_nothing_raised { env = @request.read(client) }
+    assert_equal '', env['REQUEST_PATH']
+    assert_equal '', env['PATH_INFO']
+    assert_equal '*', env['REQUEST_URI']
+    assert_nothing_raised { res = @lint.call(env) }
+  end
+
+  def test_absolute_uri_with_query
+    client = MockRequest.new("GET http://e:3/x?y=z HTTP/1.1\r\n" \
+                             "Host: foo\r\n\r\n")
+    res = env = nil
+    assert_nothing_raised { env = @request.read(client) }
+    assert_equal '/x', env['REQUEST_PATH']
+    assert_equal '/x', env['PATH_INFO']
+    assert_equal 'y=z', env['QUERY_STRING']
+    assert_nothing_raised { res = @lint.call(env) }
+  end
+
+  def test_absolute_uri_with_fragment
+    client = MockRequest.new("GET http://e:3/x#frag HTTP/1.1\r\n" \
+                             "Host: foo\r\n\r\n")
+    res = env = nil
+    assert_nothing_raised { env = @request.read(client) }
+    assert_equal '/x', env['REQUEST_PATH']
+    assert_equal '/x', env['PATH_INFO']
+    assert_equal '', env['QUERY_STRING']
+    assert_equal 'frag', env['FRAGMENT']
+    assert_nothing_raised { res = @lint.call(env) }
+  end
+
+  def test_absolute_uri_with_query_and_fragment
+    client = MockRequest.new("GET http://e:3/x?a=b#frag HTTP/1.1\r\n" \
+                             "Host: foo\r\n\r\n")
+    res = env = nil
+    assert_nothing_raised { env = @request.read(client) }
+    assert_equal '/x', env['REQUEST_PATH']
+    assert_equal '/x', env['PATH_INFO']
+    assert_equal 'a=b', env['QUERY_STRING']
+    assert_equal 'frag', env['FRAGMENT']
+    assert_nothing_raised { res = @lint.call(env) }
+  end
+
+  def test_absolute_uri_unsupported_schemes
+    %w(ssh+http://e/ ftp://e/x http+ssh://e/x).each do |abs_uri|
+      client = MockRequest.new("GET #{abs_uri} HTTP/1.1\r\n" \
+                               "Host: foo\r\n\r\n")
+      assert_raises(HttpParserError) { @request.read(client) }
+    end
+  end
+
+  def test_x_forwarded_proto_https
+    res = env = nil
+    client = MockRequest.new("GET / HTTP/1.1\r\n" \
+                             "X-Forwarded-Proto: https\r\n" \
+                             "Host: foo\r\n\r\n")
+    assert_nothing_raised { env = @request.read(client) }
+    assert_equal "https", env['rack.url_scheme']
+    assert_nothing_raised { res = @lint.call(env) }
+  end
+
+  def test_x_forwarded_proto_http
+    res = env = nil
+    client = MockRequest.new("GET / HTTP/1.1\r\n" \
+                             "X-Forwarded-Proto: http\r\n" \
+                             "Host: foo\r\n\r\n")
+    assert_nothing_raised { env = @request.read(client) }
+    assert_equal "http", env['rack.url_scheme']
+    assert_nothing_raised { res = @lint.call(env) }
+  end
+
+  def test_x_forwarded_proto_invalid
+    res = env = nil
+    client = MockRequest.new("GET / HTTP/1.1\r\n" \
+                             "X-Forwarded-Proto: ftp\r\n" \
+                             "Host: foo\r\n\r\n")
+    assert_nothing_raised { env = @request.read(client) }
+    assert_equal "http", env['rack.url_scheme']
+    assert_nothing_raised { res = @lint.call(env) }
+  end
+
+  def test_rack_lint_get
+    client = MockRequest.new("GET / HTTP/1.1\r\nHost: foo\r\n\r\n")
+    res = env = nil
+    assert_nothing_raised { env = @request.read(client) }
+    assert_equal "http", env['rack.url_scheme']
+    assert_equal '127.0.0.1', env['REMOTE_ADDR']
+    assert_nothing_raised { res = @lint.call(env) }
+  end
+
+  def test_rack_lint_put
+    client = MockRequest.new(
+      "PUT / HTTP/1.1\r\n" \
+      "Host: foo\r\n" \
+      "Content-Length: 5\r\n" \
+      "\r\n" \
+      "abcde")
+    res = env = nil
+    assert_nothing_raised { env = @request.read(client) }
+    assert ! env.include?(:http_body)
+    assert_nothing_raised { res = @lint.call(env) }
+  end
+
+  def test_rack_lint_big_put
+    count = 100
+    bs = 0x10000
+    buf = (' ' * bs).freeze
+    length = bs * count
+    client = Tempfile.new('big_put')
+    client.syswrite(
+      "PUT / HTTP/1.1\r\n" \
+      "Host: foo\r\n" \
+      "Content-Length: #{length}\r\n" \
+      "\r\n")
+    count.times { assert_equal bs, client.syswrite(buf) }
+    assert_equal 0, client.sysseek(0)
+    res = env = nil
+    assert_nothing_raised { env = @request.read(client) }
+    assert ! env.include?(:http_body)
+    assert_equal length, env['rack.input'].size
+    count.times { assert_equal buf, env['rack.input'].read(bs) }
+    assert_nil env['rack.input'].read(bs)
+    assert_nothing_raised { env['rack.input'].rewind }
+    assert_nothing_raised { res = @lint.call(env) }
+  end
+
+end
+
diff --git a/test/unit/test_response.rb b/test/unit/test_response.rb
index c30a141..66c2b54 100644
--- a/test/unit/test_response.rb
+++ b/test/unit/test_response.rb
@@ -13,16 +13,26 @@ class ResponseTest < Test::Unit::TestCase
   def test_response_headers
     out = StringIO.new
     HttpResponse.write(out,[200, {"X-Whatever" => "stuff"}, ["cool"]])
+    assert out.closed?
 
     assert out.length > 0, "output didn't have data"
   end
 
+  def test_response_string_status
+    out = StringIO.new
+    HttpResponse.write(out,['200', {}, []])
+    assert out.closed?
+    assert out.length > 0, "output didn't have data"
+    assert_equal 1, out.string.split(/\r\n/).grep(/^Status: 200 OK/).size
+  end
+
   def test_response_OFS_set
     old_ofs = $,
     $, = "\f\v"
     out = StringIO.new
-    HttpResponse.write(out,[200, {"X-Whatever" => "stuff"}, ["cool"]])
-    resp = out.read
+    HttpResponse.write(out,[200, {"X-k" => "cd","X-y" => "z"}, ["cool"]])
+    assert out.closed?
+    resp = out.string
     assert ! resp.include?("\f\v"), "output didn't use $, ($OFS)"
     ensure
       $, = old_ofs
@@ -31,6 +41,7 @@ class ResponseTest < Test::Unit::TestCase
   def test_response_200
     io = StringIO.new
     HttpResponse.write(io, [200, {}, []])
+    assert io.closed?
     assert io.length > 0, "output didn't have data"
   end
 
@@ -38,8 +49,49 @@ class ResponseTest < Test::Unit::TestCase
     code = 400
     io = StringIO.new
     HttpResponse.write(io, [code, {}, []])
-    io.rewind
-    assert_match(/.* #{HTTP_STATUS_CODES[code]}$/, io.readline.chomp, "wrong default reason phrase")
+    assert io.closed?
+    lines = io.string.split(/\r\n/)
+    assert_match(/.* Bad Request$/, lines.first,
+                 "wrong default reason phrase")
   end
-end
 
+  def test_rack_multivalue_headers
+    out = StringIO.new
+    HttpResponse.write(out,[200, {"X-Whatever" => "stuff\nbleh"}, []])
+    assert out.closed?
+    assert_match(/^X-Whatever: stuff\r\nX-Whatever: bleh\r\n/, out.string)
+  end
+
+  # Even though Rack explicitly forbids "Status" in the header hash,
+  # some broken clients still rely on it
+  def test_status_header_added
+    out = StringIO.new
+    HttpResponse.write(out,[200, {"X-Whatever" => "stuff"}, []])
+    assert out.closed?
+    assert_equal 1, out.string.split(/\r\n/).grep(/^Status: 200 OK/i).size
+  end
+
+  # we always favor the code returned by the application, since "Status"
+  # in the header hash is not allowed by Rack (but not every app is
+  # fully Rack-compliant).
+  def test_status_header_ignores_app_hash
+    out = StringIO.new
+    header_hash = {"X-Whatever" => "stuff", 'StaTus' => "666" }
+    HttpResponse.write(out,[200, header_hash, []])
+    assert out.closed?
+    assert_equal 1, out.string.split(/\r\n/).grep(/^Status: 200 OK/i).size
+    assert_equal 1, out.string.split(/\r\n/).grep(/^Status:/i).size
+  end
+
+  def test_body_closed
+    expect_body = %w(1 2 3 4).join("\n")
+    body = StringIO.new(expect_body)
+    body.rewind
+    out = StringIO.new
+    HttpResponse.write(out,[200, {}, body])
+    assert out.closed?
+    assert body.closed?
+    assert_match(expect_body, out.string.split(/\r\n/).last)
+  end
+
+end
diff --git a/test/unit/test_server.rb b/test/unit/test_server.rb
index d19064c..742b240 100644
--- a/test/unit/test_server.rb
+++ b/test/unit/test_server.rb
@@ -25,8 +25,8 @@ class WebServerTest < Test::Unit::TestCase
     @tester = TestHandler.new
     redirect_test_io do
       @server = HttpServer.new(@tester, :listeners => [ "127.0.0.1:#{@port}" ] )
+      @server.start
     end
-    @server.start
   end
 
   def teardown
@@ -35,6 +35,60 @@ class WebServerTest < Test::Unit::TestCase
     end
   end
 
+  def test_preload_app_config
+    teardown
+    tmp = Tempfile.new('test_preload_app_config')
+    ObjectSpace.undefine_finalizer(tmp)
+    app = lambda { ||
+      tmp.sysseek(0)
+      tmp.truncate(0)
+      tmp.syswrite($$)
+      lambda { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ "#$$\n" ] ] }
+    }
+    redirect_test_io do
+      @server = HttpServer.new(app, :listeners => [ "127.0.0.1:#@port"] )
+      @server.start
+    end
+    results = hit(["http://localhost:#@port/"])
+    worker_pid = results[0].to_i
+    tmp.sysseek(0)
+    loader_pid = tmp.sysread(4096).to_i
+    assert_equal worker_pid, loader_pid
+    teardown
+
+    redirect_test_io do
+      @server = HttpServer.new(app, :listeners => [ "127.0.0.1:#@port"],
+                               :preload_app => true)
+      @server.start
+    end
+    results = hit(["http://localhost:#@port/"])
+    worker_pid = results[0].to_i
+    tmp.sysseek(0)
+    loader_pid = tmp.sysread(4096).to_i
+    assert_equal $$, loader_pid
+    assert worker_pid != loader_pid
+    ensure
+      tmp.close!
+  end
+
+  def test_broken_app
+    teardown
+    app = lambda { |env| raise RuntimeError, "hello" }
+    # [200, {}, []] }
+    redirect_test_io do
+      @server = HttpServer.new(app, :listeners => [ "127.0.0.1:#@port"] )
+      @server.start
+    end
+    sock = nil
+    assert_nothing_raised do
+      sock = TCPSocket.new('127.0.0.1', @port)
+      sock.syswrite("GET / HTTP/1.0\r\n\r\n")
+    end
+
+    assert_match %r{\AHTTP/1.[01] 500\b}, sock.sysread(4096)
+    assert_nothing_raised { sock.close }
+  end
+
   def test_simple_server
     results = hit(["http://localhost:#{@port}/test"])
     assert_equal 'hello!\n', results[0], "Handler didn't really run"
@@ -77,6 +131,16 @@ class WebServerTest < Test::Unit::TestCase
     end
   end
 
+  def test_bad_client_400
+    sock = nil
+    assert_nothing_raised do
+      sock = TCPSocket.new('127.0.0.1', @port)
+      sock.syswrite("GET / HTTP/1.0\r\nHost: foo\rbar\r\n\r\n")
+    end
+    assert_match %r{\AHTTP/1.[01] 400\b}, sock.sysread(4096)
+    assert_nothing_raised { sock.close }
+  end
+
   def test_header_is_too_long
     redirect_test_io do
       long = "GET /test HTTP/1.1\r\n" + ("X-Big: stuff\r\n" * 15000) + "\r\n"
diff --git a/test/unit/test_signals.rb b/test/unit/test_signals.rb
new file mode 100644
index 0000000..ef66ed6
--- /dev/null
+++ b/test/unit/test_signals.rb
@@ -0,0 +1,191 @@
+# Copyright (c) 2009 Eric Wong
+# You can redistribute it and/or modify it under the same terms as Ruby.
+#
+# Ensure we stay sane in the face of signals being sent to us
+
+require 'test/test_helper'
+
+include Unicorn
+
+class Dd
+  def initialize(bs, count)
+    @count = count
+    @buf = ' ' * bs
+  end
+
+  def each(&block)
+    @count.times { yield @buf }
+  end
+end
+
+class SignalsTest < Test::Unit::TestCase
+
+  def setup
+    @bs = 1 * 1024 * 1024
+    @count = 100
+    @port = unused_port
+    tmp = @tmp = Tempfile.new('unicorn.sock')
+    File.unlink(@tmp.path)
+    n = 0
+    tmp.chmod(0)
+    @server_opts = {
+      :listeners => [ "127.0.0.1:#@port", @tmp.path ],
+      :after_fork => lambda { |server,worker|
+        trap(:HUP) { tmp.chmod(n += 1) }
+      },
+    }
+    @server = nil
+  end
+
+  def test_worker_dies_on_dead_master
+    pid = fork {
+      app = lambda { |env| [ 200, {'X-Pid' => "#$$" }, [] ] }
+      opts = @server_opts.merge(:timeout => 3)
+      redirect_test_io { HttpServer.new(app, opts).start.join }
+    }
+    child = sock = buf = t0 = nil
+    assert_nothing_raised do
+      wait_workers_ready("test_stderr.#{pid}.log", 1)
+      sock = TCPSocket.new('127.0.0.1', @port)
+      sock.syswrite("GET / HTTP/1.0\r\n\r\n")
+      buf = sock.readpartial(4096)
+      sock.close
+      buf =~ /\bX-Pid: (\d+)\b/ or raise Exception
+      child = $1.to_i
+      wait_master_ready("test_stderr.#{pid}.log")
+      Process.kill(:KILL, pid)
+      Process.waitpid(pid)
+      t0 = Time.now
+    end
+    assert child
+    assert t0
+    assert_raises(Errno::ESRCH) { loop { Process.kill(0, child); sleep 0.2 } }
+    assert((Time.now - t0) < 60)
+  end
+
+  def test_sleepy_kill
+    rd, wr = IO.pipe
+    pid = fork {
+      rd.close
+      app = lambda { |env| wr.syswrite('.'); sleep; [ 200, {}, [] ] }
+      redirect_test_io { HttpServer.new(app, @server_opts).start.join }
+    }
+    sock = buf = nil
+    wr.close
+    assert_nothing_raised do
+      wait_workers_ready("test_stderr.#{pid}.log", 1)
+      sock = TCPSocket.new('127.0.0.1', @port)
+      sock.syswrite("GET / HTTP/1.0\r\n\r\n")
+      buf = rd.readpartial(1)
+      wait_master_ready("test_stderr.#{pid}.log")
+      Process.kill(:INT, pid)
+      Process.waitpid(pid)
+    end
+    assert_equal '.', buf
+    buf = nil
+    assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,
+                  Errno::EBADF) do
+      buf = sock.sysread(4096)
+    end
+    assert_nil buf
+    ensure
+  end
+
+  def test_timeout_slow_response
+    pid = fork {
+      app = lambda { |env| sleep }
+      opts = @server_opts.merge(:timeout => 3)
+      redirect_test_io { HttpServer.new(app, opts).start.join }
+    }
+    t0 = Time.now
+    sock = nil
+    assert_nothing_raised do
+      wait_workers_ready("test_stderr.#{pid}.log", 1)
+      sock = TCPSocket.new('127.0.0.1', @port)
+      sock.syswrite("GET / HTTP/1.0\r\n\r\n")
+    end
+
+    buf = nil
+    assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,
+                  Errno::EBADF) do
+      buf = sock.sysread(4096)
+    end
+    diff = Time.now - t0
+    assert_nil buf
+    assert diff > 1.0, "diff was #{diff.inspect}"
+    assert diff < 60.0
+    ensure
+      Process.kill(:QUIT, pid) rescue nil
+  end
+
+  def test_response_write
+    app = lambda { |env|
+      [ 200, { 'Content-Type' => 'text/plain', 'X-Pid' => Process.pid.to_s },
+        Dd.new(@bs, @count) ]
+    }
+    redirect_test_io { @server = HttpServer.new(app, @server_opts).start }
+    sock = nil
+    assert_nothing_raised do
+      wait_workers_ready("test_stderr.#{$$}.log", 1)
+      sock = TCPSocket.new('127.0.0.1', @port)
+      sock.syswrite("GET / HTTP/1.0\r\n\r\n")
+    end
+    buf = ''
+    header_len = pid = nil
+    assert_nothing_raised do
+      buf = sock.sysread(16384, buf)
+      pid = buf[/\r\nX-Pid: (\d+)\r\n/, 1].to_i
+      header_len = buf[/\A(.+?\r\n\r\n)/m, 1].size
+    end
+    read = buf.size
+    mode_before = @tmp.stat.mode
+    assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,
+                  Errno::EBADF) do
+      loop do
+        3.times { Process.kill(:HUP, pid) }
+        sock.sysread(16384, buf)
+        read += buf.size
+        3.times { Process.kill(:HUP, pid) }
+      end
+    end
+
+    redirect_test_io { @server.stop(true) }
+    # can't check for == since pending signals get merged
+    assert mode_before < @tmp.stat.mode
+    assert_equal(read - header_len, @bs * @count)
+    assert_nothing_raised { sock.close }
+  end
+
+  def test_request_read
+    app = lambda { |env|
+      [ 200, {'Content-Type'=>'text/plain', 'X-Pid'=>Process.pid.to_s}, [] ]
+    }
+    redirect_test_io { @server = HttpServer.new(app, @server_opts).start }
+    pid = nil
+
+    assert_nothing_raised do
+      wait_workers_ready("test_stderr.#{$$}.log", 1)
+      sock = TCPSocket.new('127.0.0.1', @port)
+      sock.syswrite("GET / HTTP/1.0\r\n\r\n")
+      pid = sock.sysread(4096)[/\r\nX-Pid: (\d+)\r\n/, 1].to_i
+      sock.close
+    end
+
+    sock = TCPSocket.new('127.0.0.1', @port)
+    sock.syswrite("PUT / HTTP/1.0\r\n")
+    sock.syswrite("Content-Length: #{@bs * @count}\r\n\r\n")
+    1000.times { Process.kill(:HUP, pid) }
+    mode_before = @tmp.stat.mode
+    killer = fork { loop { Process.kill(:HUP, pid); sleep(0.0001) } }
+    buf = ' ' * @bs
+    @count.times { sock.syswrite(buf) }
+    Process.kill(:TERM, killer)
+    Process.waitpid2(killer)
+    redirect_test_io { @server.stop(true) }
+    # can't check for == since pending signals get merged
+    assert mode_before < @tmp.stat.mode
+    assert_equal pid, sock.sysread(4096)[/\r\nX-Pid: (\d+)\r\n/, 1].to_i
+    sock.close
+  end
+
+end
diff --git a/test/unit/test_socket_helper.rb b/test/unit/test_socket_helper.rb
new file mode 100644
index 0000000..75d9f7b
--- /dev/null
+++ b/test/unit/test_socket_helper.rb
@@ -0,0 +1,131 @@
+require 'test/test_helper'
+require 'tempfile'
+
+class TestSocketHelper < Test::Unit::TestCase
+  include Unicorn::SocketHelper
+  attr_reader :logger
+  GET_SLASH = "GET / HTTP/1.0\r\n\r\n".freeze
+
+  def setup
+    @log_tmp = Tempfile.new 'logger'
+    @logger = Logger.new(@log_tmp.path)
+    @test_addr = ENV['UNICORN_TEST_ADDR'] || '127.0.0.1'
+    GC.disable
+  end
+
+  def teardown
+    GC.enable
+  end
+
+  def test_bind_listen_tcp
+    port = unused_port @test_addr
+    @tcp_listener_name = "#@test_addr:#{port}"
+    @tcp_listener = bind_listen(@tcp_listener_name)
+    assert TCPServer === @tcp_listener
+    assert_equal @tcp_listener_name, sock_name(@tcp_listener)
+  end
+
+  def test_bind_listen_options
+    port = unused_port @test_addr
+    tcp_listener_name = "#@test_addr:#{port}"
+    tmp = Tempfile.new 'unix.sock'
+    unix_listener_name = tmp.path
+    File.unlink(tmp.path)
+    [ { :backlog => 5 }, { :sndbuf => 4096 }, { :rcvbuf => 4096 },
+      { :backlog => 16, :rcvbuf => 4096, :sndbuf => 4096 }
+    ].each do |opts|
+      assert_nothing_raised do
+        tcp_listener = bind_listen(tcp_listener_name, opts)
+        assert TCPServer === tcp_listener
+        tcp_listener.close
+        unix_listener = bind_listen(unix_listener_name, opts)
+        assert UNIXServer === unix_listener
+        unix_listener.close
+      end
+    end
+    #system('cat', @log_tmp.path)
+  end
+
+  def test_bind_listen_unix
+    old_umask = File.umask(0777)
+    tmp = Tempfile.new 'unix.sock'
+    @unix_listener_path = tmp.path
+    File.unlink(@unix_listener_path)
+    @unix_listener = bind_listen(@unix_listener_path)
+    assert UNIXServer === @unix_listener
+    assert_equal @unix_listener_path, sock_name(@unix_listener)
+    assert File.readable?(@unix_listener_path), "not readable"
+    assert File.writable?(@unix_listener_path), "not writable"
+    assert_equal 0777, File.umask
+    ensure
+      File.umask(old_umask)
+  end
+
+  def test_bind_listen_unix_idempotent
+    test_bind_listen_unix
+    a = bind_listen(@unix_listener)
+    assert_equal a.fileno, @unix_listener.fileno
+    unix_server = server_cast(@unix_listener)
+    assert UNIXServer === unix_server
+    a = bind_listen(unix_server)
+    assert_equal a.fileno, unix_server.fileno
+    assert_equal a.fileno, @unix_listener.fileno
+  end
+
+  def test_bind_listen_tcp_idempotent
+    test_bind_listen_tcp
+    a = bind_listen(@tcp_listener)
+    assert_equal a.fileno, @tcp_listener.fileno
+    tcp_server = server_cast(@tcp_listener)
+    assert TCPServer === tcp_server
+    a = bind_listen(tcp_server)
+    assert_equal a.fileno, tcp_server.fileno
+    assert_equal a.fileno, @tcp_listener.fileno
+  end
+
+  def test_bind_listen_unix_rebind
+    test_bind_listen_unix
+    new_listener = bind_listen(@unix_listener_path)
+    assert UNIXServer === new_listener
+    assert new_listener.fileno != @unix_listener.fileno
+    assert_equal sock_name(new_listener), sock_name(@unix_listener)
+    assert_equal @unix_listener_path, sock_name(new_listener)
+    pid = fork do
+      client = server_cast(new_listener).accept
+      client.syswrite('abcde')
+      exit 0
+    end
+    s = UNIXSocket.new(@unix_listener_path)
+    IO.select([s])
+    assert_equal 'abcde', s.sysread(5)
+    pid, status = Process.waitpid2(pid)
+    assert status.success?
+  end
+
+  def test_server_cast
+    assert_nothing_raised do
+      test_bind_listen_unix
+      test_bind_listen_tcp
+    end
+    unix_listener_socket = Socket.for_fd(@unix_listener.fileno)
+    assert Socket === unix_listener_socket
+    @unix_server = server_cast(unix_listener_socket)
+    assert_equal @unix_listener.fileno, @unix_server.fileno
+    assert UNIXServer === @unix_server
+    assert File.socket?(@unix_server.path)
+    assert_equal @unix_listener_path, sock_name(@unix_server)
+
+    tcp_listener_socket = Socket.for_fd(@tcp_listener.fileno)
+    assert Socket === tcp_listener_socket
+    @tcp_server = server_cast(tcp_listener_socket)
+    assert_equal @tcp_listener.fileno, @tcp_server.fileno
+    assert TCPServer === @tcp_server
+    assert_equal @tcp_listener_name, sock_name(@tcp_server)
+  end
+
+  def test_sock_name
+    test_server_cast
+    sock_name(@unix_server)
+  end
+
+end
diff --git a/test/unit/test_upload.rb b/test/unit/test_upload.rb
index edc94da..9ef3ed7 100644
--- a/test/unit/test_upload.rb
+++ b/test/unit/test_upload.rb
@@ -18,12 +18,29 @@ class UploadTest < Test::Unit::TestCase
     @sha1 = Digest::SHA1.new
     @sha1_app = lambda do |env|
       input = env['rack.input']
-      resp = { :pos => input.pos, :size => input.stat.size }
+      resp = { :pos => input.pos, :size => input.size, :class => input.class }
+
+      # sysread
+      @sha1.reset
       begin
         loop { @sha1.update(input.sysread(@bs)) }
       rescue EOFError
       end
       resp[:sha1] = @sha1.hexdigest
+
+      # read
+      input.sysseek(0) if input.respond_to?(:sysseek)
+      input.rewind
+      @sha1.reset
+      loop {
+        buf = input.read(@bs) or break
+        @sha1.update(buf)
+      }
+
+      if resp[:sha1] == @sha1.hexdigest
+        resp[:sysread_read_byte_match] = true
+      end
+
       [ 200, @hdr.merge({'X-Resp' => resp.inspect}), [] ]
     end
   end
@@ -50,6 +67,61 @@ class UploadTest < Test::Unit::TestCase
     assert_equal @sha1.hexdigest, resp[:sha1]
   end
 
+  def test_put_trickle_small
+    @count, @bs = 2, 128
+    start_server(@sha1_app)
+    assert_equal 256, length
+    sock = TCPSocket.new(@addr, @port)
+    hdr = "PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n"
+    @count.times do
+      buf = @random.sysread(@bs)
+      @sha1.update(buf)
+      hdr << buf
+      sock.syswrite(hdr)
+      hdr = ''
+      sleep 0.6
+    end
+    read = sock.read.split(/\r\n/)
+    assert_equal "HTTP/1.1 200 OK", read[0]
+    resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
+    assert_equal length, resp[:size]
+    assert_equal 0, resp[:pos]
+    assert_equal @sha1.hexdigest, resp[:sha1]
+    assert_equal StringIO, resp[:class]
+  end
+
+  def test_tempfile_unlinked
+    spew_path = lambda do |env|
+      if orig = env['HTTP_X_OLD_PATH']
+        assert orig != env['rack.input'].path
+      end
+      assert_equal length, env['rack.input'].size
+      [ 200, @hdr.merge('X-Tempfile-Path' => env['rack.input'].path), [] ]
+    end
+    start_server(spew_path)
+    sock = TCPSocket.new(@addr, @port)
+    sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
+    @count.times { sock.syswrite(' ' * @bs) }
+    path = sock.read[/^X-Tempfile-Path: (\S+)/, 1]
+    sock.close
+
+    # send another request to ensure we hit the next request
+    sock = TCPSocket.new(@addr, @port)
+    sock.syswrite("PUT / HTTP/1.0\r\nX-Old-Path: #{path}\r\n" \
+                  "Content-Length: #{length}\r\n\r\n")
+    @count.times { sock.syswrite(' ' * @bs) }
+    path2 = sock.read[/^X-Tempfile-Path: (\S+)/, 1]
+    sock.close
+    assert path != path2
+
+    # make sure the next request comes in so the unlink got processed
+    sock = TCPSocket.new(@addr, @port)
+    sock.syswrite("GET ?lasdf\r\n\r\n\r\n\r\n")
+    sock.sysread(4096) rescue nil
+    sock.close
+
+    assert ! File.exist?(path)
+  end
 
   def test_put_keepalive_truncates_small_overwrite
     start_server(@sha1_app)
@@ -135,6 +207,52 @@ class UploadTest < Test::Unit::TestCase
     assert_equal resp[:size], new_tmp.stat.size
   end
 
+  # Despite reading numerous articles and inspecting the 1.9.1-p0 C
+  # source, Eric Wong will never trust that we're always handling
+  # encoding-aware IO objects correctly.  Thus this test uses shell
+  # utilities that should always operate on files/sockets on a
+  # byte-level.
+  def test_uncomfortable_with_onenine_encodings
+    # POSIX doesn't require all of these to be present on a system
+    which('curl') or return
+    which('sha1sum') or return
+    which('dd') or return
+
+    start_server(@sha1_app)
+
+    tmp = Tempfile.new('dd_dest')
+    assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
+                        "bs=#{@bs}", "count=#{@count}"),
+           "dd #@random to #{tmp}")
+    sha1_re = %r!\b([a-f0-9]{40})\b!
+    sha1_out = `sha1sum #{tmp.path}`
+    assert $?.success?, 'sha1sum ran OK'
+
+    assert_match(sha1_re, sha1_out)
+    sha1 = sha1_re.match(sha1_out)[1]
+    resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/`
+    assert $?.success?, 'curl ran OK'
+    assert_match(%r!\b#{sha1}\b!, resp)
+    assert_match(/Tempfile/, resp)
+    assert_match(/sysread_read_byte_match/, resp)
+
+    # small StringIO path
+    assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
+                        "bs=1024", "count=1"),
+           "dd #@random to #{tmp}")
+    sha1_re = %r!\b([a-f0-9]{40})\b!
+    sha1_out = `sha1sum #{tmp.path}`
+    assert $?.success?, 'sha1sum ran OK'
+
+    assert_match(sha1_re, sha1_out)
+    sha1 = sha1_re.match(sha1_out)[1]
+    resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/`
+    assert $?.success?, 'curl ran OK'
+    assert_match(%r!\b#{sha1}\b!, resp)
+    assert_match(/StringIO/, resp)
+    assert_match(/sysread_read_byte_match/, resp)
+  end
+
   private
 
   def length
diff --git a/test/unit/test_util.rb b/test/unit/test_util.rb
new file mode 100644
index 0000000..1616eac
--- /dev/null
+++ b/test/unit/test_util.rb
@@ -0,0 +1,87 @@
+require 'test/test_helper'
+require 'tempfile'
+
+class TestUtil < Test::Unit::TestCase
+
+  EXPECT_FLAGS = File::WRONLY | File::APPEND
+  def test_reopen_logs_noop
+    tmp = Tempfile.new(nil)
+    tmp.reopen(tmp.path, 'a')
+    tmp.sync = true
+    ext = tmp.external_encoding rescue nil
+    int = tmp.internal_encoding rescue nil
+    before = tmp.stat.inspect
+    Unicorn::Util.reopen_logs
+    assert_equal before, File.stat(tmp.path).inspect
+    assert_equal ext, (tmp.external_encoding rescue nil)
+    assert_equal int, (tmp.internal_encoding rescue nil)
+  end
+
+  def test_reopen_logs_renamed
+    tmp = Tempfile.new(nil)
+    tmp_path = tmp.path.freeze
+    tmp.reopen(tmp_path, 'a')
+    tmp.sync = true
+    ext = tmp.external_encoding rescue nil
+    int = tmp.internal_encoding rescue nil
+    before = tmp.stat.inspect
+    to = Tempfile.new(nil)
+    File.rename(tmp_path, to.path)
+    assert ! File.exist?(tmp_path)
+    Unicorn::Util.reopen_logs
+    assert_equal tmp_path, tmp.path
+    assert File.exist?(tmp_path)
+    assert before != File.stat(tmp_path).inspect
+    assert_equal tmp.stat.inspect, File.stat(tmp_path).inspect
+    assert_equal ext, (tmp.external_encoding rescue nil)
+    assert_equal int, (tmp.internal_encoding rescue nil)
+    assert_equal(EXPECT_FLAGS, EXPECT_FLAGS & tmp.fcntl(Fcntl::F_GETFL))
+    assert tmp.sync
+  end
+
+  def test_reopen_logs_renamed_with_encoding
+    tmp = Tempfile.new(nil)
+    tmp_path = tmp.path.dup.freeze
+    Encoding.list.each { |encoding|
+      tmp.reopen(tmp_path, "a:#{encoding.to_s}")
+      tmp.sync = true
+      assert_equal encoding, tmp.external_encoding
+      assert_nil tmp.internal_encoding
+      File.unlink(tmp_path)
+      assert ! File.exist?(tmp_path)
+      Unicorn::Util.reopen_logs
+      assert_equal tmp_path, tmp.path
+      assert File.exist?(tmp_path)
+      assert_equal tmp.stat.inspect, File.stat(tmp_path).inspect
+      assert_equal encoding, tmp.external_encoding
+      assert_nil tmp.internal_encoding
+      assert_equal(EXPECT_FLAGS, EXPECT_FLAGS & tmp.fcntl(Fcntl::F_GETFL))
+      assert tmp.sync
+    }
+  end if STDIN.respond_to?(:external_encoding)
+
+  def test_reopen_logs_renamed_with_internal_encoding
+    tmp = Tempfile.new(nil)
+    tmp_path = tmp.path.dup.freeze
+    Encoding.list.each { |ext|
+      Encoding.list.each { |int|
+        next if ext == int
+        tmp.reopen(tmp_path, "a:#{ext.to_s}:#{int.to_s}")
+        tmp.sync = true
+        assert_equal ext, tmp.external_encoding
+        assert_equal int, tmp.internal_encoding
+        File.unlink(tmp_path)
+        assert ! File.exist?(tmp_path)
+        Unicorn::Util.reopen_logs
+        assert_equal tmp_path, tmp.path
+        assert File.exist?(tmp_path)
+        assert_equal tmp.stat.inspect, File.stat(tmp_path).inspect
+        assert_equal ext, tmp.external_encoding
+        assert_equal int, tmp.internal_encoding
+        assert_equal(EXPECT_FLAGS, EXPECT_FLAGS & tmp.fcntl(Fcntl::F_GETFL))
+        assert tmp.sync
+      }
+    }
+  end if STDIN.respond_to?(:external_encoding)
+
+end