diff options
Diffstat (limited to 'test')
-rw-r--r-- | test/rails/test_rails.rb | 30 | ||||
-rw-r--r-- | test/test_helper.rb | 26 | ||||
-rw-r--r-- | test/unit/test_chunked_reader.rb | 180 | ||||
-rw-r--r-- | test/unit/test_configurator.rb | 27 | ||||
-rw-r--r-- | test/unit/test_http_parser.rb | 30 | ||||
-rw-r--r-- | test/unit/test_request.rb | 9 | ||||
-rw-r--r-- | test/unit/test_server.rb | 13 | ||||
-rw-r--r-- | test/unit/test_signals.rb | 2 | ||||
-rw-r--r-- | test/unit/test_trailer_parser.rb | 52 | ||||
-rw-r--r-- | test/unit/test_upload.rb | 234 |
10 files changed, 473 insertions, 130 deletions
diff --git a/test/rails/test_rails.rb b/test/rails/test_rails.rb index c7add20..e6f6a36 100644 --- a/test/rails/test_rails.rb +++ b/test/rails/test_rails.rb @@ -142,18 +142,24 @@ logger Logger.new('#{COMMON_TMP.path}') 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 + + # fixed in Rack commit 44ed4640f077504a49b7f1cabf8d6ad7a13f6441, + # no released version of Rails or Rack has this fix + if RB_V[0] >= 1 && RB_V[1] >= 9 + warn "multipart broken with Rack 1.0.0 and Rails 2.3.2.1 under 1.9" + else + 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 + end # make sure we can get 403 responses, too uri = URI.parse("http://#@addr:#@port/foo/xpost") diff --git a/test/test_helper.rb b/test/test_helper.rb index 787adbf..0f2f311 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -262,3 +262,29 @@ def wait_for_death(pid) end raise "PID:#{pid} never died!" end + +# executes +cmd+ and chunks its STDOUT +def chunked_spawn(stdout, *cmd) + fork { + crd, cwr = IO.pipe + crd.binmode + cwr.binmode + crd.sync = cwr.sync = true + + pid = fork { + STDOUT.reopen(cwr) + crd.close + cwr.close + exec(*cmd) + } + cwr.close + begin + buf = crd.readpartial(16384) + stdout.write("#{'%x' % buf.size}\r\n#{buf}") + rescue EOFError + stdout.write("0\r\n") + pid, status = Process.waitpid(pid) + exit status.exitstatus + end while true + } +end diff --git a/test/unit/test_chunked_reader.rb b/test/unit/test_chunked_reader.rb new file mode 100644 index 0000000..67fe43b --- /dev/null +++ b/test/unit/test_chunked_reader.rb @@ -0,0 +1,180 @@ +require 'test/unit' +require 'unicorn' +require 'unicorn/http11' +require 'tempfile' +require 'io/nonblock' +require 'digest/sha1' + +class TestChunkedReader < Test::Unit::TestCase + + def setup + @env = {} + @rd, @wr = IO.pipe + @rd.binmode + @wr.binmode + @rd.sync = @wr.sync = true + @start_pid = $$ + end + + def teardown + return if $$ != @start_pid + @rd.close rescue nil + @wr.close rescue nil + begin + Process.wait + rescue Errno::ECHILD + break + end while true + end + + def test_error + cr = bin_reader("8\r\nasdfasdf\r\n8\r\nasdfasdfa#{'a' * 1024}") + a = nil + assert_nothing_raised { a = cr.readpartial(8192) } + assert_equal 'asdfasdf', a + assert_nothing_raised { a = cr.readpartial(8192) } + assert_equal 'asdfasdf', a + assert_raises(Unicorn::HttpParserError) { cr.readpartial(8192) } + end + + def test_eof1 + cr = bin_reader("0\r\n") + assert_raises(EOFError) { cr.readpartial(8192) } + end + + def test_eof2 + cr = bin_reader("0\r\n\r\n") + assert_raises(EOFError) { cr.readpartial(8192) } + end + + def test_readpartial1 + cr = bin_reader("4\r\nasdf\r\n0\r\n") + assert_equal 'asdf', cr.readpartial(8192) + assert_raises(EOFError) { cr.readpartial(8192) } + end + + def test_gets1 + cr = bin_reader("4\r\nasdf\r\n0\r\n") + STDOUT.sync = true + assert_equal 'asdf', cr.gets + assert_raises(EOFError) { cr.readpartial(8192) } + end + + def test_gets2 + cr = bin_reader("4\r\nasd\n\r\n0\r\n\r\n") + assert_equal "asd\n", cr.gets + assert_nil cr.gets + end + + def test_gets3 + max = Unicorn::Const::CHUNK_SIZE * 2 + str = ('a' * max).freeze + first = 5 + last = str.size - first + cr = bin_reader( + "#{'%x' % first}\r\n#{str[0, first]}\r\n" \ + "#{'%x' % last}\r\n#{str[-last, last]}\r\n" \ + "0\r\n") + assert_equal str, cr.gets + assert_nil cr.gets + end + + def test_readpartial_gets_mixed1 + max = Unicorn::Const::CHUNK_SIZE * 2 + str = ('a' * max).freeze + first = 5 + last = str.size - first + cr = bin_reader( + "#{'%x' % first}\r\n#{str[0, first]}\r\n" \ + "#{'%x' % last}\r\n#{str[-last, last]}\r\n" \ + "0\r\n") + partial = cr.readpartial(16384) + assert String === partial + + len = max - partial.size + assert_equal(str[-len, len], cr.gets) + assert_raises(EOFError) { cr.readpartial(1) } + assert_nil cr.gets + end + + def test_gets_mixed_readpartial + max = 10 + str = ("z\n" * max).freeze + first = 5 + last = str.size - first + cr = bin_reader( + "#{'%x' % first}\r\n#{str[0, first]}\r\n" \ + "#{'%x' % last}\r\n#{str[-last, last]}\r\n" \ + "0\r\n") + assert_equal("z\n", cr.gets) + assert_equal("z\n", cr.gets) + end + + def test_dd + cr = bin_reader("6\r\nhello\n\r\n") + tmp = Tempfile.new('test_dd') + tmp.sync = true + + pid = fork { + crd, cwr = IO.pipe + crd.binmode + cwr.binmode + crd.sync = cwr.sync = true + + pid = fork { + STDOUT.reopen(cwr) + crd.close + cwr.close + exec('dd', 'if=/dev/urandom', 'bs=93390', 'count=16') + } + cwr.close + begin + buf = crd.readpartial(16384) + tmp.write(buf) + @wr.write("#{'%x' % buf.size}\r\n#{buf}\r\n") + rescue EOFError + @wr.write("0\r\n\r\n") + Process.waitpid(pid) + exit 0 + end while true + } + assert_equal "hello\n", cr.gets + sha1 = Digest::SHA1.new + buf = Unicorn::Z.dup + begin + cr.readpartial(16384, buf) + sha1.update(buf) + rescue EOFError + break + end while true + + assert_nothing_raised { Process.waitpid(pid) } + sha1_file = Digest::SHA1.new + File.open(tmp.path, 'rb') { |fp| + while fp.read(16384, buf) + sha1_file.update(buf) + end + } + assert_equal sha1_file.hexdigest, sha1.hexdigest + end + + def test_trailer + @env['HTTP_TRAILER'] = 'Content-MD5' + pid = fork { @wr.syswrite("Content-MD5: asdf\r\n") } + cr = bin_reader("8\r\nasdfasdf\r\n8\r\nasdfasdf\r\n0\r\n") + assert_equal 'asdfasdf', cr.readpartial(4096) + assert_equal 'asdfasdf', cr.readpartial(4096) + assert_raises(EOFError) { cr.readpartial(4096) } + pid, status = Process.waitpid2(pid) + assert status.success? + assert_equal 'asdf', @env['HTTP_CONTENT_MD5'] + end + +private + + def bin_reader(buf) + buf.force_encoding(Encoding::BINARY) if buf.respond_to?(:force_encoding) + Unicorn::ChunkedReader.new(@env, @rd, buf) + end + +end diff --git a/test/unit/test_configurator.rb b/test/unit/test_configurator.rb index 98f2db6..aa29f61 100644 --- a/test/unit/test_configurator.rb +++ b/test/unit/test_configurator.rb @@ -1,7 +1,9 @@ require 'test/unit' require 'tempfile' -require 'unicorn/configurator' +require 'unicorn' +TestStruct = Struct.new( + *(Unicorn::Configurator::DEFAULTS.keys + %w(listener_opts listeners))) class TestConfigurator < Test::Unit::TestCase def test_config_init @@ -51,22 +53,23 @@ class TestConfigurator < Test::Unit::TestCase def test_config_defaults cfg = Unicorn::Configurator.new(:use_defaults => true) - assert_nothing_raised { cfg.commit!(self) } + test_struct = TestStruct.new + assert_nothing_raised { cfg.commit!(test_struct) } Unicorn::Configurator::DEFAULTS.each do |key,value| - assert_equal value, instance_variable_get("@#{key.to_s}") + assert_equal value, test_struct.__send__(key) end end def test_config_defaults_skip cfg = Unicorn::Configurator.new(:use_defaults => true) skip = [ :logger ] - assert_nothing_raised { cfg.commit!(self, :skip => skip) } - @logger = nil + test_struct = TestStruct.new + assert_nothing_raised { cfg.commit!(test_struct, :skip => skip) } Unicorn::Configurator::DEFAULTS.each do |key,value| next if skip.include?(key) - assert_equal value, instance_variable_get("@#{key.to_s}") + assert_equal value, test_struct.__send__(key) end - assert_nil @logger + assert_nil test_struct.logger end def test_listen_options @@ -78,8 +81,9 @@ class TestConfigurator < Test::Unit::TestCase 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")) + test_struct = TestStruct.new + assert_nothing_raised { cfg.commit!(test_struct) } + assert(listener_opts = test_struct.listener_opts) assert_equal expect, listener_opts[listener] end @@ -94,9 +98,10 @@ class TestConfigurator < Test::Unit::TestCase end def test_after_fork_proc + test_struct = TestStruct.new [ 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 + Unicorn::Configurator.new(:after_fork => my_proc).commit!(test_struct) + assert_equal my_proc, test_struct.after_fork end end diff --git a/test/unit/test_http_parser.rb b/test/unit/test_http_parser.rb index a158ebb..560f8d4 100644 --- a/test/unit/test_http_parser.rb +++ b/test/unit/test_http_parser.rb @@ -23,6 +23,7 @@ class HttpParserTest < Test::Unit::TestCase assert_equal 'GET', req['REQUEST_METHOD'] assert_nil req['FRAGMENT'] assert_equal '', req['QUERY_STRING'] + assert_nil req[:http_body] parser.reset req.clear @@ -41,6 +42,7 @@ class HttpParserTest < Test::Unit::TestCase assert_equal 'GET', req['REQUEST_METHOD'] assert_nil req['FRAGMENT'] assert_equal '', req['QUERY_STRING'] + assert_nil req[:http_body] end def test_parse_server_host_default_port @@ -49,6 +51,7 @@ class HttpParserTest < Test::Unit::TestCase 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'] + assert_nil req[:http_body] end def test_parse_server_host_alt_port @@ -57,6 +60,7 @@ class HttpParserTest < Test::Unit::TestCase 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'] + assert_nil req[:http_body] end def test_parse_server_host_empty_port @@ -65,6 +69,7 @@ class HttpParserTest < Test::Unit::TestCase 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'] + assert_nil req[:http_body] end def test_parse_server_host_xfp_https @@ -74,6 +79,7 @@ class HttpParserTest < Test::Unit::TestCase "X-Forwarded-Proto: https\r\n\r\n") assert_equal 'foo', req['SERVER_NAME'] assert_equal '443', req['SERVER_PORT'] + assert_nil req[:http_body] end def test_parse_strange_headers @@ -81,6 +87,7 @@ class HttpParserTest < Test::Unit::TestCase req = {} should_be_good = "GET / HTTP/1.1\r\naaaaaaaaaaaaa:++++++++++\r\n\r\n" assert parser.execute(req, should_be_good) + assert_nil req[:http_body] # 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, @@ -104,6 +111,7 @@ class HttpParserTest < Test::Unit::TestCase req = {} sorta_safe = %(GET #{path} HTTP/1.1\r\n\r\n) assert parser.execute(req, sorta_safe) + assert_nil req[:http_body] end end @@ -115,6 +123,7 @@ class HttpParserTest < Test::Unit::TestCase assert_raises(HttpParserError) { parser.execute(req, bad_http) } parser.reset assert(parser.execute({}, "GET / HTTP/1.0\r\n\r\n")) + assert_nil req[:http_body] end def test_piecemeal @@ -134,6 +143,7 @@ class HttpParserTest < Test::Unit::TestCase assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL'] assert_nil req['FRAGMENT'] assert_equal '', req['QUERY_STRING'] + assert_nil req[:http_body] end # not common, but underscores do appear in practice @@ -150,6 +160,7 @@ class HttpParserTest < Test::Unit::TestCase assert_equal 'under_score.example.com', req['HTTP_HOST'] assert_equal 'under_score.example.com', req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] + assert_nil req[:http_body] end def test_absolute_uri @@ -243,6 +254,24 @@ class HttpParserTest < Test::Unit::TestCase assert_equal "", req[:http_body] end + def test_unknown_methods + %w(GETT HEADR XGET XHEAD).each { |m| + parser = HttpParser.new + req = {} + s = "#{m} /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n" + ok = false + assert_nothing_raised do + ok = parser.execute(req, s) + end + 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'] + assert_equal "", req[:http_body] + assert_equal m, req['REQUEST_METHOD'] + } + end + def test_fragment_in_uri parser = HttpParser.new req = {} @@ -255,6 +284,7 @@ class HttpParserTest < Test::Unit::TestCase assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI'] assert_equal 'posts-17408', req['FRAGMENT'] assert_equal 'page=1', req['QUERY_STRING'] + assert_nil req[:http_body] end # lame random garbage maker diff --git a/test/unit/test_request.rb b/test/unit/test_request.rb index 0bfff7d..139fc82 100644 --- a/test/unit/test_request.rb +++ b/test/unit/test_request.rb @@ -16,10 +16,11 @@ class RequestTest < Test::Unit::TestCase class MockRequest < StringIO alias_method :readpartial, :sysread + alias_method :read_nonblock, :sysread end def setup - @request = HttpRequest.new(Logger.new($stderr)) + @request = HttpRequest.new @app = lambda do |env| [ 200, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ] end @@ -149,7 +150,11 @@ class RequestTest < Test::Unit::TestCase 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) } + count.times { + tmp = env['rack.input'].read(bs) + tmp << env['rack.input'].read(bs - tmp.size) if tmp.size != bs + assert_equal buf, tmp + } assert_nil env['rack.input'].read(bs) assert_nothing_raised { env['rack.input'].rewind } assert_nothing_raised { res = @lint.call(env) } diff --git a/test/unit/test_server.rb b/test/unit/test_server.rb index 742b240..22b9934 100644 --- a/test/unit/test_server.rb +++ b/test/unit/test_server.rb @@ -12,6 +12,8 @@ class TestHandler def call(env) # response.socket.write("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhello!\n") + while env['rack.input'].read(4096) + end [200, { 'Content-Type' => 'text/plain' }, ['hello!\n']] end end @@ -152,9 +154,18 @@ class WebServerTest < Test::Unit::TestCase def test_file_streamed_request body = "a" * (Unicorn::Const::MAX_BODY * 2) - long = "GET /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body + long = "PUT /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body do_test(long, Unicorn::Const::CHUNK_SIZE * 2 -400) end + def test_file_streamed_request_bad_method + body = "a" * (Unicorn::Const::MAX_BODY * 2) + long = "GET /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body + assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL, + Errno::EBADF) { + do_test(long, Unicorn::Const::CHUNK_SIZE * 2 -400) + } + end + end diff --git a/test/unit/test_signals.rb b/test/unit/test_signals.rb index ef66ed6..8ac4707 100644 --- a/test/unit/test_signals.rb +++ b/test/unit/test_signals.rb @@ -158,6 +158,8 @@ class SignalsTest < Test::Unit::TestCase def test_request_read app = lambda { |env| + while env['rack.input'].read(4096) + end [ 200, {'Content-Type'=>'text/plain', 'X-Pid'=>Process.pid.to_s}, [] ] } redirect_test_io { @server = HttpServer.new(app, @server_opts).start } diff --git a/test/unit/test_trailer_parser.rb b/test/unit/test_trailer_parser.rb new file mode 100644 index 0000000..840e9ad --- /dev/null +++ b/test/unit/test_trailer_parser.rb @@ -0,0 +1,52 @@ +require 'test/unit' +require 'unicorn' +require 'unicorn/http11' +require 'unicorn/trailer_parser' + +class TestTrailerParser < Test::Unit::TestCase + + def test_basic + tp = Unicorn::TrailerParser.new('Content-MD5') + env = {} + assert ! tp.execute!(env, "Content-MD5: asdf") + assert env.empty? + assert tp.execute!(env, "Content-MD5: asdf\r\n") + assert_equal 'asdf', env['HTTP_CONTENT_MD5'] + assert_equal 1, env.size + end + + def test_invalid_trailer + tp = Unicorn::TrailerParser.new('Content-MD5') + env = {} + assert_raises(Unicorn::HttpParserError) { + tp.execute!(env, "Content-MD: asdf\r\n") + } + assert env.empty? + end + + def test_multiple_trailer + tp = Unicorn::TrailerParser.new('Foo,Bar') + env = {} + buf = "Bar: a\r\nFoo: b\r\n" + assert tp.execute!(env, buf) + assert_equal 'a', env['HTTP_BAR'] + assert_equal 'b', env['HTTP_FOO'] + end + + def test_too_big_key + tp = Unicorn::TrailerParser.new('Foo,Bar') + env = {} + buf = "Bar#{'a' * 1024}: a\r\nFoo: b\r\n" + assert_raises(Unicorn::HttpParserError) { tp.execute!(env, buf) } + assert env.empty? + end + + def test_too_big_value + tp = Unicorn::TrailerParser.new('Foo,Bar') + env = {} + buf = "Bar: #{'a' * (1024 * 1024)}: a\r\nFoo: b\r\n" + assert_raises(Unicorn::HttpParserError) { tp.execute!(env, buf) } + assert env.empty? + end + +end diff --git a/test/unit/test_upload.rb b/test/unit/test_upload.rb index 9ef3ed7..dad5825 100644 --- a/test/unit/test_upload.rb +++ b/test/unit/test_upload.rb @@ -1,5 +1,6 @@ # Copyright (c) 2009 Eric Wong require 'test/test_helper' +require 'digest/md5' include Unicorn @@ -18,29 +19,33 @@ class UploadTest < Test::Unit::TestCase @sha1 = Digest::SHA1.new @sha1_app = lambda do |env| input = env['rack.input'] - resp = { :pos => input.pos, :size => input.size, :class => input.class } + resp = {} - # sysread @sha1.reset - begin - loop { @sha1.update(input.sysread(@bs)) } - rescue EOFError + while buf = input.read(@bs) + @sha1.update(buf) end resp[:sha1] = @sha1.hexdigest - # read - input.sysseek(0) if input.respond_to?(:sysseek) + # rewind and read again input.rewind @sha1.reset - loop { - buf = input.read(@bs) or break + while buf = input.read(@bs) @sha1.update(buf) - } + end if resp[:sha1] == @sha1.hexdigest resp[:sysread_read_byte_match] = true end + if expect_size = env['HTTP_X_EXPECT_SIZE'] + if expect_size.to_i == input.size + resp[:expect_size_match] = true + end + end + resp[:size] = input.size + resp[:content_md5] = env['HTTP_CONTENT_MD5'] + [ 200, @hdr.merge({'X-Resp' => resp.inspect}), [] ] end end @@ -54,7 +59,7 @@ class UploadTest < Test::Unit::TestCase start_server(@sha1_app) sock = TCPSocket.new(@addr, @port) sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n") - @count.times do + @count.times do |i| buf = @random.sysread(@bs) @sha1.update(buf) sock.syswrite(buf) @@ -63,10 +68,34 @@ class UploadTest < Test::Unit::TestCase 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] end + def test_put_content_md5 + md5 = Digest::MD5.new + start_server(@sha1_app) + sock = TCPSocket.new(@addr, @port) + sock.syswrite("PUT / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n" \ + "Trailer: Content-MD5\r\n\r\n") + @count.times do |i| + buf = @random.sysread(@bs) + @sha1.update(buf) + md5.update(buf) + sock.syswrite("#{'%x' % buf.size}\r\n") + sock.syswrite(buf << "\r\n") + end + sock.syswrite("0\r\n") + + content_md5 = [ md5.digest! ].pack('m').strip.freeze + sock.syswrite("Content-MD5: #{content_md5}\r\n") + 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 @sha1.hexdigest, resp[:sha1] + assert_equal content_md5, resp[:content_md5] + end + def test_put_trickle_small @count, @bs = 2, 128 start_server(@sha1_app) @@ -85,42 +114,7 @@ class UploadTest < Test::Unit::TestCase 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 @@ -136,75 +130,31 @@ class UploadTest < Test::Unit::TestCase sock.syswrite('12345') # write 4 bytes more than we expected @sha1.update('1') - read = sock.read.split(/\r\n/) + buf = sock.readpartial(4096) + while buf !~ /\r\n\r\n/ + buf << sock.readpartial(4096) + end + read = buf.split(/\r\n/) assert_equal "HTTP/1.1 200 OK", read[0] resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, '')) assert_equal to_upload, resp[:size] - assert_equal 0, resp[:pos] assert_equal @sha1.hexdigest, resp[:sha1] end def test_put_excessive_overwrite_closed - start_server(lambda { |env| [ 200, @hdr, [] ] }) - sock = TCPSocket.new(@addr, @port) - buf = ' ' * @bs - sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n") - @count.times { sock.syswrite(buf) } - assert_raise(Errno::ECONNRESET, Errno::EPIPE) do - ::Unicorn::Const::CHUNK_SIZE.times { sock.syswrite(buf) } - end - end - - def test_put_handler_closed_file - nr = '0' start_server(lambda { |env| - env['rack.input'].close - resp = { :nr => nr.succ! } - [ 200, @hdr.merge({ 'X-Resp' => resp.inspect}), [] ] + while env['rack.input'].read(65536); end + [ 200, @hdr, [] ] }) sock = TCPSocket.new(@addr, @port) buf = ' ' * @bs sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n") - @count.times { sock.syswrite(buf) } - 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 '1', resp[:nr] - - # server still alive? - sock = TCPSocket.new(@addr, @port) - sock.syswrite("GET / HTTP/1.0\r\n\r\n") - 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 '2', resp[:nr] - end - def test_renamed_file_not_closed - start_server(lambda { |env| - new_tmp = Tempfile.new('unicorn_test') - input = env['rack.input'] - File.rename(input.path, new_tmp.path) - resp = { - :inode => input.stat.ino, - :size => input.stat.size, - :new_tmp => new_tmp.path, - :old_tmp => input.path, - } - [ 200, @hdr.merge({ 'X-Resp' => resp.inspect}), [] ] - }) - sock = TCPSocket.new(@addr, @port) - buf = ' ' * @bs - sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n") @count.times { sock.syswrite(buf) } - 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: /, '')) - new_tmp = File.open(resp[:new_tmp]) - assert_equal resp[:inode], new_tmp.stat.ino - assert_equal length, resp[:size] - assert ! File.exist?(resp[:old_tmp]) - assert_equal resp[:size], new_tmp.stat.size + assert_raise(Errno::ECONNRESET, Errno::EPIPE) do + ::Unicorn::Const::CHUNK_SIZE.times { sock.syswrite(buf) } + end + assert_equal "HTTP/1.1 200 OK\r\n", sock.gets end # Despite reading numerous articles and inspecting the 1.9.1-p0 C @@ -233,7 +183,6 @@ class UploadTest < Test::Unit::TestCase 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 @@ -249,10 +198,87 @@ class UploadTest < Test::Unit::TestCase 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 + def test_chunked_upload_via_curl + # 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] + cmd = "curl -H 'X-Expect-Size: #{tmp.size}' --tcp-nodelay \ + -isSf --no-buffer -T- " \ + "http://#@addr:#@port/" + resp = Tempfile.new('resp') + resp.sync = true + + rd, wr = IO.pipe + wr.sync = rd.sync = true + pid = fork { + STDIN.reopen(rd) + rd.close + wr.close + STDOUT.reopen(resp) + exec cmd + } + rd.close + + tmp.rewind + @count.times { |i| + wr.write(tmp.read(@bs)) + sleep(rand / 10) if 0 == i % 8 + } + wr.close + pid, status = Process.waitpid2(pid) + + resp.rewind + resp = resp.read + assert status.success?, 'curl ran OK' + assert_match(%r!\b#{sha1}\b!, resp) + assert_match(/sysread_read_byte_match/, resp) + assert_match(/expect_size_match/, resp) + end + + def test_curl_chunked_small + # 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') + # 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 -H 'X-Expect-Size: #{tmp.size}' --tcp-nodelay \ + -isSf --no-buffer -T- http://#@addr:#@port/ < #{tmp.path}` + assert $?.success?, 'curl ran OK' + assert_match(%r!\b#{sha1}\b!, resp) + assert_match(/sysread_read_byte_match/, resp) + assert_match(/expect_size_match/, resp) + end + private def length |