unicorn 0.8.4 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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