unicorn 0.8.4 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,180 @@
1
+ require 'test/unit'
2
+ require 'unicorn'
3
+ require 'unicorn/http11'
4
+ require 'tempfile'
5
+ require 'io/nonblock'
6
+ require 'digest/sha1'
7
+
8
+ class TestChunkedReader < Test::Unit::TestCase
9
+
10
+ def setup
11
+ @env = {}
12
+ @rd, @wr = IO.pipe
13
+ @rd.binmode
14
+ @wr.binmode
15
+ @rd.sync = @wr.sync = true
16
+ @start_pid = $$
17
+ end
18
+
19
+ def teardown
20
+ return if $$ != @start_pid
21
+ @rd.close rescue nil
22
+ @wr.close rescue nil
23
+ begin
24
+ Process.wait
25
+ rescue Errno::ECHILD
26
+ break
27
+ end while true
28
+ end
29
+
30
+ def test_error
31
+ cr = bin_reader("8\r\nasdfasdf\r\n8\r\nasdfasdfa#{'a' * 1024}")
32
+ a = nil
33
+ assert_nothing_raised { a = cr.readpartial(8192) }
34
+ assert_equal 'asdfasdf', a
35
+ assert_nothing_raised { a = cr.readpartial(8192) }
36
+ assert_equal 'asdfasdf', a
37
+ assert_raises(Unicorn::HttpParserError) { cr.readpartial(8192) }
38
+ end
39
+
40
+ def test_eof1
41
+ cr = bin_reader("0\r\n")
42
+ assert_raises(EOFError) { cr.readpartial(8192) }
43
+ end
44
+
45
+ def test_eof2
46
+ cr = bin_reader("0\r\n\r\n")
47
+ assert_raises(EOFError) { cr.readpartial(8192) }
48
+ end
49
+
50
+ def test_readpartial1
51
+ cr = bin_reader("4\r\nasdf\r\n0\r\n")
52
+ assert_equal 'asdf', cr.readpartial(8192)
53
+ assert_raises(EOFError) { cr.readpartial(8192) }
54
+ end
55
+
56
+ def test_gets1
57
+ cr = bin_reader("4\r\nasdf\r\n0\r\n")
58
+ STDOUT.sync = true
59
+ assert_equal 'asdf', cr.gets
60
+ assert_raises(EOFError) { cr.readpartial(8192) }
61
+ end
62
+
63
+ def test_gets2
64
+ cr = bin_reader("4\r\nasd\n\r\n0\r\n\r\n")
65
+ assert_equal "asd\n", cr.gets
66
+ assert_nil cr.gets
67
+ end
68
+
69
+ def test_gets3
70
+ max = Unicorn::Const::CHUNK_SIZE * 2
71
+ str = ('a' * max).freeze
72
+ first = 5
73
+ last = str.size - first
74
+ cr = bin_reader(
75
+ "#{'%x' % first}\r\n#{str[0, first]}\r\n" \
76
+ "#{'%x' % last}\r\n#{str[-last, last]}\r\n" \
77
+ "0\r\n")
78
+ assert_equal str, cr.gets
79
+ assert_nil cr.gets
80
+ end
81
+
82
+ def test_readpartial_gets_mixed1
83
+ max = Unicorn::Const::CHUNK_SIZE * 2
84
+ str = ('a' * max).freeze
85
+ first = 5
86
+ last = str.size - first
87
+ cr = bin_reader(
88
+ "#{'%x' % first}\r\n#{str[0, first]}\r\n" \
89
+ "#{'%x' % last}\r\n#{str[-last, last]}\r\n" \
90
+ "0\r\n")
91
+ partial = cr.readpartial(16384)
92
+ assert String === partial
93
+
94
+ len = max - partial.size
95
+ assert_equal(str[-len, len], cr.gets)
96
+ assert_raises(EOFError) { cr.readpartial(1) }
97
+ assert_nil cr.gets
98
+ end
99
+
100
+ def test_gets_mixed_readpartial
101
+ max = 10
102
+ str = ("z\n" * max).freeze
103
+ first = 5
104
+ last = str.size - first
105
+ cr = bin_reader(
106
+ "#{'%x' % first}\r\n#{str[0, first]}\r\n" \
107
+ "#{'%x' % last}\r\n#{str[-last, last]}\r\n" \
108
+ "0\r\n")
109
+ assert_equal("z\n", cr.gets)
110
+ assert_equal("z\n", cr.gets)
111
+ end
112
+
113
+ def test_dd
114
+ cr = bin_reader("6\r\nhello\n\r\n")
115
+ tmp = Tempfile.new('test_dd')
116
+ tmp.sync = true
117
+
118
+ pid = fork {
119
+ crd, cwr = IO.pipe
120
+ crd.binmode
121
+ cwr.binmode
122
+ crd.sync = cwr.sync = true
123
+
124
+ pid = fork {
125
+ STDOUT.reopen(cwr)
126
+ crd.close
127
+ cwr.close
128
+ exec('dd', 'if=/dev/urandom', 'bs=93390', 'count=16')
129
+ }
130
+ cwr.close
131
+ begin
132
+ buf = crd.readpartial(16384)
133
+ tmp.write(buf)
134
+ @wr.write("#{'%x' % buf.size}\r\n#{buf}\r\n")
135
+ rescue EOFError
136
+ @wr.write("0\r\n\r\n")
137
+ Process.waitpid(pid)
138
+ exit 0
139
+ end while true
140
+ }
141
+ assert_equal "hello\n", cr.gets
142
+ sha1 = Digest::SHA1.new
143
+ buf = Unicorn::Z.dup
144
+ begin
145
+ cr.readpartial(16384, buf)
146
+ sha1.update(buf)
147
+ rescue EOFError
148
+ break
149
+ end while true
150
+
151
+ assert_nothing_raised { Process.waitpid(pid) }
152
+ sha1_file = Digest::SHA1.new
153
+ File.open(tmp.path, 'rb') { |fp|
154
+ while fp.read(16384, buf)
155
+ sha1_file.update(buf)
156
+ end
157
+ }
158
+ assert_equal sha1_file.hexdigest, sha1.hexdigest
159
+ end
160
+
161
+ def test_trailer
162
+ @env['HTTP_TRAILER'] = 'Content-MD5'
163
+ pid = fork { @wr.syswrite("Content-MD5: asdf\r\n") }
164
+ cr = bin_reader("8\r\nasdfasdf\r\n8\r\nasdfasdf\r\n0\r\n")
165
+ assert_equal 'asdfasdf', cr.readpartial(4096)
166
+ assert_equal 'asdfasdf', cr.readpartial(4096)
167
+ assert_raises(EOFError) { cr.readpartial(4096) }
168
+ pid, status = Process.waitpid2(pid)
169
+ assert status.success?
170
+ assert_equal 'asdf', @env['HTTP_CONTENT_MD5']
171
+ end
172
+
173
+ private
174
+
175
+ def bin_reader(buf)
176
+ buf.force_encoding(Encoding::BINARY) if buf.respond_to?(:force_encoding)
177
+ Unicorn::ChunkedReader.new(@env, @rd, buf)
178
+ end
179
+
180
+ end
@@ -1,6 +1,6 @@
1
1
  require 'test/unit'
2
2
  require 'tempfile'
3
- require 'unicorn/configurator'
3
+ require 'unicorn'
4
4
 
5
5
  class TestConfigurator < Test::Unit::TestCase
6
6
 
@@ -23,6 +23,7 @@ class HttpParserTest < Test::Unit::TestCase
23
23
  assert_equal 'GET', req['REQUEST_METHOD']
24
24
  assert_nil req['FRAGMENT']
25
25
  assert_equal '', req['QUERY_STRING']
26
+ assert_nil req[:http_body]
26
27
 
27
28
  parser.reset
28
29
  req.clear
@@ -41,6 +42,7 @@ class HttpParserTest < Test::Unit::TestCase
41
42
  assert_equal 'GET', req['REQUEST_METHOD']
42
43
  assert_nil req['FRAGMENT']
43
44
  assert_equal '', req['QUERY_STRING']
45
+ assert_nil req[:http_body]
44
46
  end
45
47
 
46
48
  def test_parse_server_host_default_port
@@ -49,6 +51,7 @@ class HttpParserTest < Test::Unit::TestCase
49
51
  assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo\r\n\r\n")
50
52
  assert_equal 'foo', req['SERVER_NAME']
51
53
  assert_equal '80', req['SERVER_PORT']
54
+ assert_nil req[:http_body]
52
55
  end
53
56
 
54
57
  def test_parse_server_host_alt_port
@@ -57,6 +60,7 @@ class HttpParserTest < Test::Unit::TestCase
57
60
  assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:999\r\n\r\n")
58
61
  assert_equal 'foo', req['SERVER_NAME']
59
62
  assert_equal '999', req['SERVER_PORT']
63
+ assert_nil req[:http_body]
60
64
  end
61
65
 
62
66
  def test_parse_server_host_empty_port
@@ -65,6 +69,7 @@ class HttpParserTest < Test::Unit::TestCase
65
69
  assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:\r\n\r\n")
66
70
  assert_equal 'foo', req['SERVER_NAME']
67
71
  assert_equal '80', req['SERVER_PORT']
72
+ assert_nil req[:http_body]
68
73
  end
69
74
 
70
75
  def test_parse_server_host_xfp_https
@@ -74,6 +79,7 @@ class HttpParserTest < Test::Unit::TestCase
74
79
  "X-Forwarded-Proto: https\r\n\r\n")
75
80
  assert_equal 'foo', req['SERVER_NAME']
76
81
  assert_equal '443', req['SERVER_PORT']
82
+ assert_nil req[:http_body]
77
83
  end
78
84
 
79
85
  def test_parse_strange_headers
@@ -81,6 +87,7 @@ class HttpParserTest < Test::Unit::TestCase
81
87
  req = {}
82
88
  should_be_good = "GET / HTTP/1.1\r\naaaaaaaaaaaaa:++++++++++\r\n\r\n"
83
89
  assert parser.execute(req, should_be_good)
90
+ assert_nil req[:http_body]
84
91
 
85
92
  # ref: http://thread.gmane.org/gmane.comp.lang.ruby.mongrel.devel/37/focus=45
86
93
  # (note we got 'pen' mixed up with 'pound' in that thread,
@@ -104,6 +111,7 @@ class HttpParserTest < Test::Unit::TestCase
104
111
  req = {}
105
112
  sorta_safe = %(GET #{path} HTTP/1.1\r\n\r\n)
106
113
  assert parser.execute(req, sorta_safe)
114
+ assert_nil req[:http_body]
107
115
  end
108
116
  end
109
117
 
@@ -115,6 +123,7 @@ class HttpParserTest < Test::Unit::TestCase
115
123
  assert_raises(HttpParserError) { parser.execute(req, bad_http) }
116
124
  parser.reset
117
125
  assert(parser.execute({}, "GET / HTTP/1.0\r\n\r\n"))
126
+ assert_nil req[:http_body]
118
127
  end
119
128
 
120
129
  def test_piecemeal
@@ -134,6 +143,7 @@ class HttpParserTest < Test::Unit::TestCase
134
143
  assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
135
144
  assert_nil req['FRAGMENT']
136
145
  assert_equal '', req['QUERY_STRING']
146
+ assert_nil req[:http_body]
137
147
  end
138
148
 
139
149
  # not common, but underscores do appear in practice
@@ -150,6 +160,7 @@ class HttpParserTest < Test::Unit::TestCase
150
160
  assert_equal 'under_score.example.com', req['HTTP_HOST']
151
161
  assert_equal 'under_score.example.com', req['SERVER_NAME']
152
162
  assert_equal '80', req['SERVER_PORT']
163
+ assert_nil req[:http_body]
153
164
  end
154
165
 
155
166
  def test_absolute_uri
@@ -243,6 +254,24 @@ class HttpParserTest < Test::Unit::TestCase
243
254
  assert_equal "", req[:http_body]
244
255
  end
245
256
 
257
+ def test_unknown_methods
258
+ %w(GETT HEADR XGET XHEAD).each { |m|
259
+ parser = HttpParser.new
260
+ req = {}
261
+ s = "#{m} /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n"
262
+ ok = false
263
+ assert_nothing_raised do
264
+ ok = parser.execute(req, s)
265
+ end
266
+ assert ok
267
+ assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI']
268
+ assert_equal 'posts-17408', req['FRAGMENT']
269
+ assert_equal 'page=1', req['QUERY_STRING']
270
+ assert_equal "", req[:http_body]
271
+ assert_equal m, req['REQUEST_METHOD']
272
+ }
273
+ end
274
+
246
275
  def test_fragment_in_uri
247
276
  parser = HttpParser.new
248
277
  req = {}
@@ -255,6 +284,7 @@ class HttpParserTest < Test::Unit::TestCase
255
284
  assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI']
256
285
  assert_equal 'posts-17408', req['FRAGMENT']
257
286
  assert_equal 'page=1', req['QUERY_STRING']
287
+ assert_nil req[:http_body]
258
288
  end
259
289
 
260
290
  # lame random garbage maker
@@ -16,6 +16,7 @@ class RequestTest < Test::Unit::TestCase
16
16
 
17
17
  class MockRequest < StringIO
18
18
  alias_method :readpartial, :sysread
19
+ alias_method :read_nonblock, :sysread
19
20
  end
20
21
 
21
22
  def setup
@@ -149,7 +150,11 @@ class RequestTest < Test::Unit::TestCase
149
150
  assert_nothing_raised { env = @request.read(client) }
150
151
  assert ! env.include?(:http_body)
151
152
  assert_equal length, env['rack.input'].size
152
- count.times { assert_equal buf, env['rack.input'].read(bs) }
153
+ count.times {
154
+ tmp = env['rack.input'].read(bs)
155
+ tmp << env['rack.input'].read(bs - tmp.size) if tmp.size != bs
156
+ assert_equal buf, tmp
157
+ }
153
158
  assert_nil env['rack.input'].read(bs)
154
159
  assert_nothing_raised { env['rack.input'].rewind }
155
160
  assert_nothing_raised { res = @lint.call(env) }
@@ -12,6 +12,8 @@ class TestHandler
12
12
 
13
13
  def call(env)
14
14
  # response.socket.write("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhello!\n")
15
+ while env['rack.input'].read(4096)
16
+ end
15
17
  [200, { 'Content-Type' => 'text/plain' }, ['hello!\n']]
16
18
  end
17
19
  end
@@ -152,9 +154,18 @@ class WebServerTest < Test::Unit::TestCase
152
154
 
153
155
  def test_file_streamed_request
154
156
  body = "a" * (Unicorn::Const::MAX_BODY * 2)
155
- long = "GET /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body
157
+ long = "PUT /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body
156
158
  do_test(long, Unicorn::Const::CHUNK_SIZE * 2 -400)
157
159
  end
158
160
 
161
+ def test_file_streamed_request_bad_method
162
+ body = "a" * (Unicorn::Const::MAX_BODY * 2)
163
+ long = "GET /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body
164
+ assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,
165
+ Errno::EBADF) {
166
+ do_test(long, Unicorn::Const::CHUNK_SIZE * 2 -400)
167
+ }
168
+ end
169
+
159
170
  end
160
171
 
@@ -158,6 +158,8 @@ class SignalsTest < Test::Unit::TestCase
158
158
 
159
159
  def test_request_read
160
160
  app = lambda { |env|
161
+ while env['rack.input'].read(4096)
162
+ end
161
163
  [ 200, {'Content-Type'=>'text/plain', 'X-Pid'=>Process.pid.to_s}, [] ]
162
164
  }
163
165
  redirect_test_io { @server = HttpServer.new(app, @server_opts).start }
@@ -0,0 +1,52 @@
1
+ require 'test/unit'
2
+ require 'unicorn'
3
+ require 'unicorn/http11'
4
+ require 'unicorn/trailer_parser'
5
+
6
+ class TestTrailerParser < Test::Unit::TestCase
7
+
8
+ def test_basic
9
+ tp = Unicorn::TrailerParser.new('Content-MD5')
10
+ env = {}
11
+ assert ! tp.execute!(env, "Content-MD5: asdf")
12
+ assert env.empty?
13
+ assert tp.execute!(env, "Content-MD5: asdf\r\n")
14
+ assert_equal 'asdf', env['HTTP_CONTENT_MD5']
15
+ assert_equal 1, env.size
16
+ end
17
+
18
+ def test_invalid_trailer
19
+ tp = Unicorn::TrailerParser.new('Content-MD5')
20
+ env = {}
21
+ assert_raises(Unicorn::HttpParserError) {
22
+ tp.execute!(env, "Content-MD: asdf\r\n")
23
+ }
24
+ assert env.empty?
25
+ end
26
+
27
+ def test_multiple_trailer
28
+ tp = Unicorn::TrailerParser.new('Foo,Bar')
29
+ env = {}
30
+ buf = "Bar: a\r\nFoo: b\r\n"
31
+ assert tp.execute!(env, buf)
32
+ assert_equal 'a', env['HTTP_BAR']
33
+ assert_equal 'b', env['HTTP_FOO']
34
+ end
35
+
36
+ def test_too_big_key
37
+ tp = Unicorn::TrailerParser.new('Foo,Bar')
38
+ env = {}
39
+ buf = "Bar#{'a' * 1024}: a\r\nFoo: b\r\n"
40
+ assert_raises(Unicorn::HttpParserError) { tp.execute!(env, buf) }
41
+ assert env.empty?
42
+ end
43
+
44
+ def test_too_big_value
45
+ tp = Unicorn::TrailerParser.new('Foo,Bar')
46
+ env = {}
47
+ buf = "Bar: #{'a' * (1024 * 1024)}: a\r\nFoo: b\r\n"
48
+ assert_raises(Unicorn::HttpParserError) { tp.execute!(env, buf) }
49
+ assert env.empty?
50
+ end
51
+
52
+ end
@@ -1,5 +1,6 @@
1
1
  # Copyright (c) 2009 Eric Wong
2
2
  require 'test/test_helper'
3
+ require 'digest/md5'
3
4
 
4
5
  include Unicorn
5
6
 
@@ -18,29 +19,33 @@ class UploadTest < Test::Unit::TestCase
18
19
  @sha1 = Digest::SHA1.new
19
20
  @sha1_app = lambda do |env|
20
21
  input = env['rack.input']
21
- resp = { :pos => input.pos, :size => input.size, :class => input.class }
22
+ resp = {}
22
23
 
23
- # sysread
24
24
  @sha1.reset
25
- begin
26
- loop { @sha1.update(input.sysread(@bs)) }
27
- rescue EOFError
25
+ while buf = input.read(@bs)
26
+ @sha1.update(buf)
28
27
  end
29
28
  resp[:sha1] = @sha1.hexdigest
30
29
 
31
- # read
32
- input.sysseek(0) if input.respond_to?(:sysseek)
30
+ # rewind and read again
33
31
  input.rewind
34
32
  @sha1.reset
35
- loop {
36
- buf = input.read(@bs) or break
33
+ while buf = input.read(@bs)
37
34
  @sha1.update(buf)
38
- }
35
+ end
39
36
 
40
37
  if resp[:sha1] == @sha1.hexdigest
41
38
  resp[:sysread_read_byte_match] = true
42
39
  end
43
40
 
41
+ if expect_size = env['HTTP_X_EXPECT_SIZE']
42
+ if expect_size.to_i == input.size
43
+ resp[:expect_size_match] = true
44
+ end
45
+ end
46
+ resp[:size] = input.size
47
+ resp[:content_md5] = env['HTTP_CONTENT_MD5']
48
+
44
49
  [ 200, @hdr.merge({'X-Resp' => resp.inspect}), [] ]
45
50
  end
46
51
  end
@@ -54,7 +59,7 @@ class UploadTest < Test::Unit::TestCase
54
59
  start_server(@sha1_app)
55
60
  sock = TCPSocket.new(@addr, @port)
56
61
  sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
57
- @count.times do
62
+ @count.times do |i|
58
63
  buf = @random.sysread(@bs)
59
64
  @sha1.update(buf)
60
65
  sock.syswrite(buf)
@@ -63,10 +68,34 @@ class UploadTest < Test::Unit::TestCase
63
68
  assert_equal "HTTP/1.1 200 OK", read[0]
64
69
  resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
65
70
  assert_equal length, resp[:size]
66
- assert_equal 0, resp[:pos]
67
71
  assert_equal @sha1.hexdigest, resp[:sha1]
68
72
  end
69
73
 
74
+ def test_put_content_md5
75
+ md5 = Digest::MD5.new
76
+ start_server(@sha1_app)
77
+ sock = TCPSocket.new(@addr, @port)
78
+ sock.syswrite("PUT / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n" \
79
+ "Trailer: Content-MD5\r\n\r\n")
80
+ @count.times do |i|
81
+ buf = @random.sysread(@bs)
82
+ @sha1.update(buf)
83
+ md5.update(buf)
84
+ sock.syswrite("#{'%x' % buf.size}\r\n")
85
+ sock.syswrite(buf << "\r\n")
86
+ end
87
+ sock.syswrite("0\r\n")
88
+
89
+ content_md5 = [ md5.digest! ].pack('m').strip.freeze
90
+ sock.syswrite("Content-MD5: #{content_md5}\r\n")
91
+ read = sock.read.split(/\r\n/)
92
+ assert_equal "HTTP/1.1 200 OK", read[0]
93
+ resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
94
+ assert_equal length, resp[:size]
95
+ assert_equal @sha1.hexdigest, resp[:sha1]
96
+ assert_equal content_md5, resp[:content_md5]
97
+ end
98
+
70
99
  def test_put_trickle_small
71
100
  @count, @bs = 2, 128
72
101
  start_server(@sha1_app)
@@ -85,42 +114,7 @@ class UploadTest < Test::Unit::TestCase
85
114
  assert_equal "HTTP/1.1 200 OK", read[0]
86
115
  resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
87
116
  assert_equal length, resp[:size]
88
- assert_equal 0, resp[:pos]
89
117
  assert_equal @sha1.hexdigest, resp[:sha1]
90
- assert_equal StringIO, resp[:class]
91
- end
92
-
93
- def test_tempfile_unlinked
94
- spew_path = lambda do |env|
95
- if orig = env['HTTP_X_OLD_PATH']
96
- assert orig != env['rack.input'].path
97
- end
98
- assert_equal length, env['rack.input'].size
99
- [ 200, @hdr.merge('X-Tempfile-Path' => env['rack.input'].path), [] ]
100
- end
101
- start_server(spew_path)
102
- sock = TCPSocket.new(@addr, @port)
103
- sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
104
- @count.times { sock.syswrite(' ' * @bs) }
105
- path = sock.read[/^X-Tempfile-Path: (\S+)/, 1]
106
- sock.close
107
-
108
- # send another request to ensure we hit the next request
109
- sock = TCPSocket.new(@addr, @port)
110
- sock.syswrite("PUT / HTTP/1.0\r\nX-Old-Path: #{path}\r\n" \
111
- "Content-Length: #{length}\r\n\r\n")
112
- @count.times { sock.syswrite(' ' * @bs) }
113
- path2 = sock.read[/^X-Tempfile-Path: (\S+)/, 1]
114
- sock.close
115
- assert path != path2
116
-
117
- # make sure the next request comes in so the unlink got processed
118
- sock = TCPSocket.new(@addr, @port)
119
- sock.syswrite("GET ?lasdf\r\n\r\n\r\n\r\n")
120
- sock.sysread(4096) rescue nil
121
- sock.close
122
-
123
- assert ! File.exist?(path)
124
118
  end
125
119
 
126
120
  def test_put_keepalive_truncates_small_overwrite
@@ -136,75 +130,31 @@ class UploadTest < Test::Unit::TestCase
136
130
  sock.syswrite('12345') # write 4 bytes more than we expected
137
131
  @sha1.update('1')
138
132
 
139
- read = sock.read.split(/\r\n/)
133
+ buf = sock.readpartial(4096)
134
+ while buf !~ /\r\n\r\n/
135
+ buf << sock.readpartial(4096)
136
+ end
137
+ read = buf.split(/\r\n/)
140
138
  assert_equal "HTTP/1.1 200 OK", read[0]
141
139
  resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
142
140
  assert_equal to_upload, resp[:size]
143
- assert_equal 0, resp[:pos]
144
141
  assert_equal @sha1.hexdigest, resp[:sha1]
145
142
  end
146
143
 
147
144
  def test_put_excessive_overwrite_closed
148
- start_server(lambda { |env| [ 200, @hdr, [] ] })
149
- sock = TCPSocket.new(@addr, @port)
150
- buf = ' ' * @bs
151
- sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
152
- @count.times { sock.syswrite(buf) }
153
- assert_raise(Errno::ECONNRESET, Errno::EPIPE) do
154
- ::Unicorn::Const::CHUNK_SIZE.times { sock.syswrite(buf) }
155
- end
156
- end
157
-
158
- def test_put_handler_closed_file
159
- nr = '0'
160
145
  start_server(lambda { |env|
161
- env['rack.input'].close
162
- resp = { :nr => nr.succ! }
163
- [ 200, @hdr.merge({ 'X-Resp' => resp.inspect}), [] ]
146
+ while env['rack.input'].read(65536); end
147
+ [ 200, @hdr, [] ]
164
148
  })
165
149
  sock = TCPSocket.new(@addr, @port)
166
150
  buf = ' ' * @bs
167
151
  sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
168
- @count.times { sock.syswrite(buf) }
169
- read = sock.read.split(/\r\n/)
170
- assert_equal "HTTP/1.1 200 OK", read[0]
171
- resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
172
- assert_equal '1', resp[:nr]
173
-
174
- # server still alive?
175
- sock = TCPSocket.new(@addr, @port)
176
- sock.syswrite("GET / HTTP/1.0\r\n\r\n")
177
- read = sock.read.split(/\r\n/)
178
- assert_equal "HTTP/1.1 200 OK", read[0]
179
- resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
180
- assert_equal '2', resp[:nr]
181
- end
182
152
 
183
- def test_renamed_file_not_closed
184
- start_server(lambda { |env|
185
- new_tmp = Tempfile.new('unicorn_test')
186
- input = env['rack.input']
187
- File.rename(input.path, new_tmp.path)
188
- resp = {
189
- :inode => input.stat.ino,
190
- :size => input.stat.size,
191
- :new_tmp => new_tmp.path,
192
- :old_tmp => input.path,
193
- }
194
- [ 200, @hdr.merge({ 'X-Resp' => resp.inspect}), [] ]
195
- })
196
- sock = TCPSocket.new(@addr, @port)
197
- buf = ' ' * @bs
198
- sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n")
199
153
  @count.times { sock.syswrite(buf) }
200
- read = sock.read.split(/\r\n/)
201
- assert_equal "HTTP/1.1 200 OK", read[0]
202
- resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, ''))
203
- new_tmp = File.open(resp[:new_tmp])
204
- assert_equal resp[:inode], new_tmp.stat.ino
205
- assert_equal length, resp[:size]
206
- assert ! File.exist?(resp[:old_tmp])
207
- assert_equal resp[:size], new_tmp.stat.size
154
+ assert_raise(Errno::ECONNRESET, Errno::EPIPE) do
155
+ ::Unicorn::Const::CHUNK_SIZE.times { sock.syswrite(buf) }
156
+ end
157
+ assert_equal "HTTP/1.1 200 OK\r\n", sock.gets
208
158
  end
209
159
 
210
160
  # Despite reading numerous articles and inspecting the 1.9.1-p0 C
@@ -233,7 +183,6 @@ class UploadTest < Test::Unit::TestCase
233
183
  resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/`
234
184
  assert $?.success?, 'curl ran OK'
235
185
  assert_match(%r!\b#{sha1}\b!, resp)
236
- assert_match(/Tempfile/, resp)
237
186
  assert_match(/sysread_read_byte_match/, resp)
238
187
 
239
188
  # small StringIO path
@@ -249,10 +198,87 @@ class UploadTest < Test::Unit::TestCase
249
198
  resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/`
250
199
  assert $?.success?, 'curl ran OK'
251
200
  assert_match(%r!\b#{sha1}\b!, resp)
252
- assert_match(/StringIO/, resp)
253
201
  assert_match(/sysread_read_byte_match/, resp)
254
202
  end
255
203
 
204
+ def test_chunked_upload_via_curl
205
+ # POSIX doesn't require all of these to be present on a system
206
+ which('curl') or return
207
+ which('sha1sum') or return
208
+ which('dd') or return
209
+
210
+ start_server(@sha1_app)
211
+
212
+ tmp = Tempfile.new('dd_dest')
213
+ assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
214
+ "bs=#{@bs}", "count=#{@count}"),
215
+ "dd #@random to #{tmp}")
216
+ sha1_re = %r!\b([a-f0-9]{40})\b!
217
+ sha1_out = `sha1sum #{tmp.path}`
218
+ assert $?.success?, 'sha1sum ran OK'
219
+
220
+ assert_match(sha1_re, sha1_out)
221
+ sha1 = sha1_re.match(sha1_out)[1]
222
+ cmd = "curl -H 'X-Expect-Size: #{tmp.size}' --tcp-nodelay \
223
+ -isSf --no-buffer -T- " \
224
+ "http://#@addr:#@port/"
225
+ resp = Tempfile.new('resp')
226
+ resp.sync = true
227
+
228
+ rd, wr = IO.pipe
229
+ wr.sync = rd.sync = true
230
+ pid = fork {
231
+ STDIN.reopen(rd)
232
+ rd.close
233
+ wr.close
234
+ STDOUT.reopen(resp)
235
+ exec cmd
236
+ }
237
+ rd.close
238
+
239
+ tmp.rewind
240
+ @count.times { |i|
241
+ wr.write(tmp.read(@bs))
242
+ sleep(rand / 10) if 0 == i % 8
243
+ }
244
+ wr.close
245
+ pid, status = Process.waitpid2(pid)
246
+
247
+ resp.rewind
248
+ resp = resp.read
249
+ assert status.success?, 'curl ran OK'
250
+ assert_match(%r!\b#{sha1}\b!, resp)
251
+ assert_match(/sysread_read_byte_match/, resp)
252
+ assert_match(/expect_size_match/, resp)
253
+ end
254
+
255
+ def test_curl_chunked_small
256
+ # POSIX doesn't require all of these to be present on a system
257
+ which('curl') or return
258
+ which('sha1sum') or return
259
+ which('dd') or return
260
+
261
+ start_server(@sha1_app)
262
+
263
+ tmp = Tempfile.new('dd_dest')
264
+ # small StringIO path
265
+ assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}",
266
+ "bs=1024", "count=1"),
267
+ "dd #@random to #{tmp}")
268
+ sha1_re = %r!\b([a-f0-9]{40})\b!
269
+ sha1_out = `sha1sum #{tmp.path}`
270
+ assert $?.success?, 'sha1sum ran OK'
271
+
272
+ assert_match(sha1_re, sha1_out)
273
+ sha1 = sha1_re.match(sha1_out)[1]
274
+ resp = `curl -H 'X-Expect-Size: #{tmp.size}' --tcp-nodelay \
275
+ -isSf --no-buffer -T- http://#@addr:#@port/ < #{tmp.path}`
276
+ assert $?.success?, 'curl ran OK'
277
+ assert_match(%r!\b#{sha1}\b!, resp)
278
+ assert_match(/sysread_read_byte_match/, resp)
279
+ assert_match(/expect_size_match/, resp)
280
+ end
281
+
256
282
  private
257
283
 
258
284
  def length