about summary refs log tree commit homepage
path: root/test/unit
diff options
context:
space:
mode:
Diffstat (limited to 'test/unit')
-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
9 files changed, 1067 insertions, 48 deletions
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