http_spew 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,6 +12,8 @@ class HTTP_Spew::HitNRun < HTTP_Spew::Request
12
12
  }.freeze,
13
13
  [].freeze ].freeze
14
14
 
15
+ # we close the connection once we're ready to read the response to
16
+ # save bandwidth
15
17
  def read_response
16
18
  close
17
19
  RESPONSE
@@ -3,33 +3,25 @@
3
3
  # Use this to wrap and replace your input object for spraying to multiple
4
4
  # servers.
5
5
  class HTTP_Spew::InputSpray
6
- attr_reader :readers
7
- class NoWritersError < HTTP_Spew::Error
8
- end
9
-
10
- class SizedPipe < HTTP_Spew::ChunkyPipe
11
- attr_accessor :size
12
- end
13
-
14
- def initialize(env, nr)
15
- @input = env["rack.input"]
16
- size = @input.respond_to?(:size) ? @input.size : env["CONTENT_LENGTH"]
17
- size = size ? size.to_i : nil
18
- klass = size ? SizedPipe : HTTP_Spew::ChunkyPipe
19
- @readers, @writers = [], []
6
+ def initialize(env, nr, input = env["rack.input"])
7
+ @input = input
8
+ @pipes = {}.compare_by_identity
20
9
  nr.times do
21
- r, w = klass.new
22
- r.size = size if size
23
- @readers << r
24
- @writers << w
10
+ r, w = HTTP_Spew::ChunkyPipe.new
11
+ @pipes[r] = w
25
12
  end
26
- @wr = start_write_driver
13
+ start_write_driver
14
+ end
15
+
16
+ def readers
17
+ @pipes.keys
27
18
  end
28
19
 
29
- def write_fail?(wr, buf)
30
- wr.kgio_write(buf)
20
+ def write_fail?(rd, wr, buf)
21
+ wr.write(buf)
31
22
  false
32
- rescue
23
+ rescue => e
24
+ rd.error = e
33
25
  wr.close
34
26
  true
35
27
  end
@@ -39,14 +31,14 @@ class HTTP_Spew::InputSpray
39
31
  Thread.new do
40
32
  begin
41
33
  buf = ""
42
- while buf = @input.read(0x4000, buf)
43
- @writers.delete_if { |wr| write_fail?(wr, buf) }.empty? and
44
- raise NoWritersError, "all writers have died"
34
+ while @input.read(0x4000, buf)
35
+ @pipes.delete_if { |rd, wr| write_fail?(rd, wr, buf) }.empty? and
36
+ raise HTTP_Spew::NoWritersError, "all writers have died", []
45
37
  end
46
38
  rescue => e
47
- @readers.each { |io| io.error = e }
39
+ @pipes.each { |rd, _| rd.error = e }
48
40
  ensure
49
- @writers.each { |io| io.close unless io.closed? }
41
+ @pipes.each { |_, wr| wr.close unless wr.closed? }
50
42
  end
51
43
  end
52
44
  end
@@ -1,20 +1,39 @@
1
1
  # -*- encoding: binary -*-
2
+ require "io/wait"
3
+
4
+ # This is the base class actually capable of making a normal HTTP request
2
5
  class HTTP_Spew::Request
6
+
7
+ # May be called by some Rack servers such as Rainbows! to bypass
8
+ # +to_path+ calls and avoid path lookups.
3
9
  attr_reader :to_io
10
+
11
+ # Stores any exception that was raised in another thread (e.g.
12
+ # ContentMD5 or InputSpray write drivers).
4
13
  attr_reader :error
14
+
15
+ # Stores the Rack response (a 3-element Array) on success
5
16
  attr_reader :response
6
- class RequestError < HTTP_Spew::Error
7
- end
8
17
 
9
18
  include HTTP_Spew::Headers
10
19
 
11
- def initialize(env, input, sock)
20
+ # Creates a new Request based on a Rack +env+ and +input+ object and
21
+ # prepares it for writing to +sock+. +input+ supercedes env["rack.input"]
22
+ # since it may be an alternate IO object (such as one filtered through
23
+ # HTTP_Spew::ContentMD5.
24
+ #
25
+ # +sock+ may be the String representing an address created with
26
+ # +Socket.pack_sockaddr_un+ or +Socket.pack_sockaddr_in+, or it
27
+ # may be an actual IO object with Kgio::SocketMethods mixed in
28
+ # (e.g. Kgio::Socket)
29
+ def initialize(env, input, sock, allow = nil)
12
30
  @to_io = Kgio::SocketMethods === sock ? sock : Kgio::Socket.start(sock)
13
31
  if Hash === env
14
32
  @buf, @input = env_to_headers(env, input)
15
33
  else
16
34
  @buf, @input = env, input
17
35
  end
36
+ @allow = allow
18
37
  end
19
38
 
20
39
  # returns a 3-element Rack response array on completion
@@ -35,43 +54,78 @@ class HTTP_Spew::Request
35
54
  end
36
55
  end
37
56
 
57
+ # returns a 3-element Rack response array on successful completion
58
+ # returns an Exception if one was raised
59
+ def run(timeout)
60
+ t0 = Time.now
61
+ buf, @buf = @buf, nil # make inspect nicer
62
+ @to_io.write(buf)
63
+ if @input
64
+ @to_io.write(buf) while @input.read(0x4000, buf)
65
+ end
66
+ timeout -= (Time.now - t0)
67
+ while :wait_readable == (rv = read_response) && timeout >= 0.0
68
+ t0 = Time.now
69
+ @to_io.wait(timeout) if timeout > 0.0
70
+ timeout -= (Time.now - t0)
71
+ end
72
+ rv
73
+ rescue => e
74
+ @input.respond_to?(:close) and @input.close rescue nil
75
+ self.error = e
76
+ end
77
+
78
+ # returns a 3-element Rack response array on completion
79
+ # returns :wait_readable or :wait_writable if busy
80
+ # Users do not need to call this directly, +resume+ will return the result
81
+ # of this.
38
82
  def read_response
39
- buf = @to_io.kgio_trypeek(0x4000) or eof!
83
+ buf = @to_io.kgio_trypeek(0x4000) or
84
+ raise HttpSpew::EOF, "upstream server closed connection", []
40
85
  String === buf or return buf
41
86
 
42
87
  # Kcar::Parser#headers shortens +buf+ for us
43
88
  hdr_len = buf.size
44
89
  r = Kcar::Parser.new.headers({}, buf) or too_big!
90
+ if @allow && ! @allow.include?(r[0].to_i)
91
+ raise HTTP_Spew::UnexpectedResponse,
92
+ "#{r[0].to_i} not in #{@allow.inspect}", []
93
+ end
45
94
 
46
95
  # discard the header data from the socket buffer
47
96
  (hdr_len -= buf.size) > 0 and @to_io.kgio_read(hdr_len, buf)
48
- r[2] = self
49
- @response = r
97
+ @response = r << self
50
98
  end
51
99
 
100
+ # Used by some Rack-compatible servers to optimize transfers
101
+ # by using IO.copy_stream
52
102
  def to_path
53
103
  "/dev/fd/#{@to_io.fileno}"
54
104
  end
55
105
 
56
- def too_big!
57
- raise RequestError.new(self), "response headers too large", []
106
+ def too_big! # :nodoc:
107
+ raise HTTP_Spew::RequestError.new(self), "response headers too large", []
58
108
  end
59
109
 
110
+ # Called by Rack servers to write the response to a client
60
111
  def each
61
112
  buf = ""
62
- while buf = @to_io.kgio_read(0x4000, buf)
113
+ while @to_io.kgio_read(0x4000, buf)
63
114
  yield buf
64
115
  end
65
116
  end
66
117
 
118
+ # Used internally by various HTTP_Spew elements to report errors
119
+ # across different Threads and Fibers
67
120
  def error=(exception)
121
+ @input.respond_to?(:error=) and @input.error = exception
68
122
  close
69
123
  @error = exception
70
124
  end
71
125
 
72
- # this may be called by a Rack web server
126
+ # Called by Rack servers after writing a response to a client
73
127
  def close
74
128
  @to_io.close
75
- IO === @input and @input.close
129
+ @input = nil
76
130
  end
77
131
  end
@@ -0,0 +1,2 @@
1
+ # -*- encoding: binary -*-
2
+ HTTP_Spew.const_set :VERSION, "0.2.0"
data/lib/http_spew.rb CHANGED
@@ -11,87 +11,17 @@ module HTTP_Spew
11
11
  class Error < RuntimeError; end
12
12
  class TimeoutError < Error; end
13
13
  class ConnectionReset < Error; end
14
-
15
- def self.error_all(requests, error) # :nodoc:
16
- requests.each { |req| req.error ||= error }
17
- end
18
-
19
- def self.done_early(ready, failed, requests) # :nodoc:
20
- ready.concat(failed)
21
- pending = requests - ready
22
- unless pending.empty?
23
- error = ConnectionReset.new("prematurely terminated")
24
- ready.concat(error_all(pending, error))
25
- end
26
- ready.uniq!
27
- ready
28
- end
29
-
30
- # Returns an array of requests that are complete, including those
31
- # that have errored out. Incomplete requests remain in +requests+
32
- # If +need+ is fullfilled, it closes all incomplete requests and
33
- # returns all requests.
34
- def self.wait_nonblock!(need, requests)
35
- ready, failed = [], []
36
- requests.delete_if do |req|
37
- begin
38
- case req.resume
39
- when Symbol # :wait_writable, :wait_readable
40
- false
41
- else
42
- (ready << req).size == need and
43
- return done_early(ready, failed, requests)
44
- true
45
- end
46
- rescue => e
47
- req.error = e
48
- failed << req
49
- end
50
- end
51
- ready.concat(failed).empty? ? nil : ready
52
- end
53
-
54
- # Returns an array of requests that are complete, including those
55
- # that have errored out.
56
- # If +need+ is fullfilled, it closes all incomplete requests.
57
- def self.wait(need, requests, timeout)
58
- ready, failed = [], []
59
- pollset = {}
60
- begin
61
- requests.each do |req|
62
- begin
63
- case rv = req.resume
64
- when Symbol # :wait_writable, :wait_readable
65
- pollset[req] = rv
66
- else
67
- (ready << req).size == need and
68
- return done_early(ready, failed, requests)
69
- pollset.delete(req)
70
- end
71
- rescue => e
72
- req.error = e
73
- failed << req
74
- pollset.delete(req)
75
- end
76
- end
77
- break if pollset.empty?
78
-
79
- t0 = Time.now
80
- busy = pollset.keys
81
- rv = Kgio.poll(pollset, timeout.to_i) or break
82
- timeout -= (Time.now - t0) * 1000
83
- rescue Errno::EINTR
84
- timeout -= (Time.now - t0) * 1000
85
- retry
86
- end while timeout > 0.0 && requests = rv.keys.concat(busy).uniq!
87
-
88
- ready.concat(failed)
89
- unless requests.empty?
90
- ready.concat(error_all(requests, TimeoutError.new("request timed out")))
91
- ready.uniq!
92
- end
93
- ready
94
- end
14
+ class RequestError < Error; end
15
+ class UnexpectedResponse < RequestError; end
16
+ class ChecksumError < HTTP_Spew::Error; end
17
+ class LengthError < HTTP_Spew::Error; end
18
+ class NoWritersError < HTTP_Spew::Error; end
19
+ class EOF < EOFError; end
20
+
21
+ require "http_spew/version"
22
+ require "http_spew/headers"
23
+ require "http_spew/request"
24
+ require "http_spew/class_methods"
25
+
26
+ extend HTTP_Spew::ClassMethods
95
27
  end
96
- require "http_spew/headers"
97
- require "http_spew/request"
data/pkg.mk CHANGED
@@ -167,5 +167,9 @@ doc_gz: docs = $(shell find doc -type f ! -regex '^.*\.\(gif\|jpg\|png\|gz\)$$')
167
167
  doc_gz:
168
168
  for i in $(docs); do \
169
169
  gzip --rsyncable -9 < $$i > $$i.gz; touch -r $$i $$i.gz; done
170
+ check-warnings:
171
+ @(for i in $$(git ls-files '*.rb'|grep -v '^setup\.rb$$'); \
172
+ do $(RUBY) -d -W2 -c $$i; done) | grep -v '^Syntax OK$$' || :
170
173
 
171
174
  .PHONY: all .FORCE-GIT-VERSION-FILE doc test $(test_units) manifest
175
+ .PHONY: check-warnings
data/test/content-md5.ru CHANGED
@@ -2,7 +2,9 @@
2
2
  bs = ENV['bs'] ? ENV['bs'].to_i : 4096
3
3
  require 'digest/md5'
4
4
  use Rack::ContentLength
5
+ use Rack::ContentType, "text/plain"
5
6
  app = lambda do |env|
7
+ return exit!(5) if env["HTTP_X_FAIL"] == "true"
6
8
  digest = Digest::MD5.new
7
9
  input = env['rack.input']
8
10
  if buf = input.read(bs)
@@ -16,6 +18,6 @@ app = lambda do |env|
16
18
  body = "expect=#{expect}\nreaded=#{readed}\n"
17
19
  status = expect == readed ? 200 : 500
18
20
 
19
- [ status, {'Content-Type' => 'text/plain'}, [ body ] ]
21
+ [ status, {}, [ body ] ]
20
22
  end
21
23
  run app
data/test/helper.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # -*- encoding: binary -*-
2
2
  $stderr.sync = $stdout.sync = true
3
+ Thread.abort_on_exception = true
3
4
  require "test/unit"
4
5
  require "digest/sha1"
5
6
  require "stringio"
@@ -10,7 +11,8 @@ require "tempfile"
10
11
  $-w = true
11
12
  require "http_spew"
12
13
 
13
- def start_server(config, worker_processes = 4)
14
+ def start_server(config, worker_processes = 4, rewindable_input = false)
15
+ ENV["RACK_ENV"] = "deployment"
14
16
  addr = ENV["TEST_HOST"] || "127.0.0.1"
15
17
  sock = TCPServer.new(addr, 0)
16
18
  port = sock.addr[1]
@@ -21,6 +23,7 @@ def start_server(config, worker_processes = 4)
21
23
  cfg = Tempfile.new("unicorn.config")
22
24
  cfg.puts "worker_processes #{worker_processes}"
23
25
  cfg.puts "preload_app true"
26
+ cfg.puts "rewindable_input #{rewindable_input}"
24
27
  cfg.puts <<EOF
25
28
  after_fork do |s,w|
26
29
  w.nr == (s.worker_processes - 1) and File.open("#{fifo_path}", "w").close
@@ -0,0 +1,20 @@
1
+ # SHA1 checksum generator
2
+ bs = ENV['bs'] ? ENV['bs'].to_i : 16384
3
+ require 'digest/sha1'
4
+ use Rack::ContentLength
5
+ app = lambda do |env|
6
+ /\A100-continue\z/i =~ env['HTTP_EXPECT'] and
7
+ return [ 100, {}, [] ]
8
+ digest = Digest::SHA1.new
9
+ input = env['rack.input']
10
+ if buf = input.read(bs)
11
+ begin
12
+ digest.update(buf)
13
+ end while input.read(bs, buf)
14
+ end
15
+ code = env['HTTP_X_RESPONSE_CODE']
16
+ code = code.nil? ? 200 : code.to_i
17
+
18
+ [ code, {'Content-Type' => 'text/plain'}, [ digest.hexdigest << "\n" ] ]
19
+ end
20
+ run app
@@ -3,7 +3,6 @@ require "./test/helper"
3
3
 
4
4
  class TestContentMD5 < Test::Unit::TestCase
5
5
  def setup
6
- ENV["RACK_ENV"] = "deployment" # quiet Rack 1.2.1 bug
7
6
  @addr, @port, @srv = start_server("./test/content-md5.ru", 1)
8
7
  @sockaddr = Socket.pack_sockaddr_in(@port, @addr)
9
8
  @env = {
@@ -22,19 +21,21 @@ class TestContentMD5 < Test::Unit::TestCase
22
21
 
23
22
  def test_upload_with_md5
24
23
  str = rand_data(123) * (8 * 1021 * 13)
25
- expect = [Digest::MD5.digest(str)].pack("m").strip!
26
- expect = "expect=#{expect}\nreaded=#{expect}\n"
24
+ expect_md5 = [Digest::MD5.digest(str)].pack("m0")
25
+ expect = "expect=#{expect_md5}\nreaded=#{expect_md5}\n"
27
26
  @env["CONTENT_LENGTH"] = str.size.to_s
28
27
  @env["rack.input"] = StringIO.new(str)
29
- input = HTTP_Spew::ContentMD5.input(@env)
28
+ input = HTTP_Spew::ContentMD5.new(@env)
30
29
  assert_nil @env["CONTENT_LENGTH"]
31
30
  assert_equal "chunked", @env["HTTP_TRANSFER_ENCODING"]
32
31
  req = HTTP_Spew::Request.new(@env, input, @sockaddr)
33
- rv = HTTP_Spew.wait 1, [req], 666000
32
+ rv = HTTP_Spew.wait 1, [req], 666
34
33
  assert_equal 200, rv[0].response[0].to_i
35
34
  body = ""
36
35
  req.each { |buf| body << buf }
37
36
  assert_equal body, expect
37
+ assert_equal expect_md5, input.content_md5
38
+ assert_equal 123 * 8 * 1021 * 13, input.bytes_digested
38
39
  end
39
40
 
40
41
  def test_upload_with_corrupt_md5
@@ -44,12 +45,12 @@ class TestContentMD5 < Test::Unit::TestCase
44
45
  str = rand_data(123) * (8 * 1021 * 13)
45
46
  @env["CONTENT_LENGTH"] = str.size.to_s
46
47
  @env["rack.input"] = StringIO.new(str)
47
- input = HTTP_Spew::ContentMD5.input(@env)
48
+ input = HTTP_Spew::ContentMD5.new(@env)
48
49
  assert_nil @env["CONTENT_LENGTH"]
49
50
  assert_equal "chunked", @env["HTTP_TRANSFER_ENCODING"]
50
51
  req = HTTP_Spew::Request.new(@env, input, @sockaddr)
51
- rv = HTTP_Spew.wait 1, [req], 3600_000
52
- assert_kind_of HTTP_Spew::ContentMD5::MismatchError, rv[0].error
52
+ rv = HTTP_Spew.wait 1, [req], 3600
53
+ assert_kind_of HTTP_Spew::ChecksumError, rv[0].error
53
54
  end
54
55
 
55
56
  def test_upload_with_corrupt_length
@@ -59,12 +60,12 @@ class TestContentMD5 < Test::Unit::TestCase
59
60
  str = rand_data(123) * (8 * 1021 * 13)
60
61
  @env["CONTENT_LENGTH"] = (str.size + 1).to_s
61
62
  @env["rack.input"] = StringIO.new(str)
62
- input = HTTP_Spew::ContentMD5.input(@env)
63
+ input = HTTP_Spew::ContentMD5.new(@env)
63
64
  assert_nil @env["CONTENT_LENGTH"]
64
65
  assert_equal "chunked", @env["HTTP_TRANSFER_ENCODING"]
65
66
  req = HTTP_Spew::Request.new(@env, input, @sockaddr)
66
- rv = HTTP_Spew.wait 1, [req], 3600_000
67
- assert_kind_of HTTP_Spew::ContentMD5::LengthError, rv[0].error
67
+ rv = HTTP_Spew.wait 1, [req], 3600
68
+ assert_kind_of HTTP_Spew::LengthError, rv[0].error
68
69
  end
69
70
 
70
71
  def test_upload_with_valid_md5
@@ -73,11 +74,11 @@ class TestContentMD5 < Test::Unit::TestCase
73
74
  @env["HTTP_CONTENT_MD5"] = expect
74
75
  @env["CONTENT_LENGTH"] = str.size.to_s
75
76
  @env["rack.input"] = StringIO.new(str)
76
- input = HTTP_Spew::ContentMD5.input(@env)
77
+ input = HTTP_Spew::ContentMD5.new(@env)
77
78
  assert_nil @env["CONTENT_LENGTH"]
78
79
  assert_equal "chunked", @env["HTTP_TRANSFER_ENCODING"]
79
80
  req = HTTP_Spew::Request.new(@env, input, @sockaddr)
80
- rv = HTTP_Spew.wait 1, [req], 3600_000
81
+ rv = HTTP_Spew.wait 1, [req], 3600
81
82
  assert_equal 200, rv[0].response[0].to_i
82
83
  body = ""
83
84
  req.each { |buf| body << buf }
@@ -25,12 +25,12 @@ class TestInputSpray < Test::Unit::TestCase
25
25
  sha1[rd].update buf
26
26
  false
27
27
  else
28
- rd.close.nil?
28
+ true
29
29
  end
30
30
  end until readers.empty?
31
31
 
32
32
  sha1.each_value { |dig| assert_equal EXPECT, dig.hexdigest }
33
- sprayer.instance_variable_get(:@writers).each { |x| assert x.closed? }
33
+ sprayer.instance_variable_get(:@pipes).each_value { |x| assert x.closed? }
34
34
  end
35
35
 
36
36
  def test_spray_one_reader_dies
@@ -50,22 +50,21 @@ class TestInputSpray < Test::Unit::TestCase
50
50
  false
51
51
  end
52
52
  else
53
- rd.close.nil?
53
+ true
54
54
  end
55
55
  end until readers.empty?
56
56
 
57
57
  dead_sha1 = sha1.delete to_die
58
58
  assert EXPECT != dead_sha1.hexdigest
59
59
  sha1.each_value { |dig| assert_equal EXPECT, dig.hexdigest }
60
- sprayer.instance_variable_get(:@writers).each { |x| assert x.closed? }
60
+ sprayer.instance_variable_get(:@pipes).each_value { |x| assert x.closed? }
61
61
  end
62
62
 
63
63
  def test_spray_stream
64
64
  rd, wr = IO.pipe
65
65
  assert @env.delete("CONTENT_LENGTH")
66
66
  io = @env.delete("rack.input")
67
- buf = ""
68
- thr = Thread.new do
67
+ thr = Thread.new("") do |buf|
69
68
  while buf = io.read(128, buf)
70
69
  wr.write buf
71
70
  end
@@ -77,20 +76,20 @@ class TestInputSpray < Test::Unit::TestCase
77
76
  readers = sprayer.readers
78
77
  assert_equal 2, readers.size
79
78
 
80
- sha1 = Hash[readers.map { |rd| [ rd, Digest::SHA1.new ] } ]
79
+ sha1 = Hash[readers.map { |x| [ x, Digest::SHA1.new ] } ]
81
80
 
82
- readers.delete_if do |rd|
83
- assert ! rd.respond_to?(:size)
84
- if buf = rd.read(0x10000)
85
- sha1[rd].update buf
81
+ readers.delete_if do |x|
82
+ assert ! x.respond_to?(:size)
83
+ if buf = x.read(0x10000)
84
+ sha1[x].update buf
86
85
  false
87
86
  else
88
- rd.close.nil?
87
+ true
89
88
  end
90
89
  end until readers.empty?
91
90
 
92
91
  sha1.each_value { |dig| assert_equal EXPECT, dig.hexdigest }
93
- sprayer.instance_variable_get(:@writers).each { |x| assert x.closed? }
92
+ sprayer.instance_variable_get(:@pipes).each_value { |x| assert x.closed? }
94
93
  thr.join
95
94
  assert_equal :ok, thr.value
96
95
  assert ! rd.closed?
@@ -0,0 +1,107 @@
1
+ require "./test/helper"
2
+
3
+ class TestInputSprayWithMD5 < Test::Unit::TestCase
4
+ def setup
5
+ @nr = 4
6
+ @addr, @port, @srv = start_server("./test/content-md5.ru", @nr)
7
+ @sockaddr = Socket.pack_sockaddr_in(@port, @addr)
8
+ @env = {
9
+ "REQUEST_METHOD" => "PUT",
10
+ "REQUEST_URI" => "/",
11
+ "HTTP_HOST" => "example.com",
12
+ }
13
+ @tmpfiles = []
14
+ end
15
+
16
+ def teardown
17
+ Process.kill(:QUIT, @srv)
18
+ Process.waitpid2(@srv)
19
+ @tmpfiles.each { |tmp| tmp.closed? or tmp.close! }
20
+ end
21
+
22
+ def test_upload_with_valid_md5_sprayed
23
+ str = rand_data(123) * (8 * 1021 * 13)
24
+ expect_md5 = [Digest::MD5.digest(str)].pack("m0")
25
+ @env["HTTP_CONTENT_MD5"] = expect_md5
26
+ expect = "expect=#{expect_md5}\nreaded=#{expect_md5}\n"
27
+ @env["CONTENT_LENGTH"] = str.size.to_s
28
+ @env["rack.input"] = StringIO.new(str)
29
+ input = HTTP_Spew::ContentMD5.new(@env)
30
+ sprayer = HTTP_Spew::InputSpray.new(@env, @nr, input)
31
+ assert_nil @env["CONTENT_LENGTH"]
32
+ assert_equal "chunked", @env["HTTP_TRANSFER_ENCODING"]
33
+ reqs = sprayer.readers.map do |md5_input|
34
+ HTTP_Spew::Request.new(@env, md5_input, @sockaddr)
35
+ end
36
+ assert_equal @nr, reqs.size
37
+ rv = HTTP_Spew.wait_mt reqs.size, reqs, 3600
38
+ assert_equal @nr, rv.size
39
+ rv.each do |resp|
40
+ assert_equal 200, resp.response[0].to_i
41
+ body = ""
42
+ resp.response[2].each { |buf| body << buf }
43
+ assert_equal expect, body
44
+ end
45
+ assert_equal expect_md5, input.content_md5
46
+ assert_equal 123 * 8 * 1021 * 13, input.bytes_digested
47
+ end
48
+
49
+ def test_upload_with_invalid_md5_sprayed
50
+ str = rand_data(123) * (8 * 1021 * 13)
51
+ expect = [Digest::MD5.digest(str + "HI")].pack("m0")
52
+ @env["HTTP_CONTENT_MD5"] = expect
53
+ @env["CONTENT_LENGTH"] = str.size.to_s
54
+ @env["rack.input"] = StringIO.new(str)
55
+ input = HTTP_Spew::ContentMD5.new(@env)
56
+ sprayer = HTTP_Spew::InputSpray.new(@env, @nr, input)
57
+ assert_nil @env["CONTENT_LENGTH"]
58
+ assert_equal "chunked", @env["HTTP_TRANSFER_ENCODING"]
59
+ reqs = sprayer.readers.map do |md5_input|
60
+ HTTP_Spew::Request.new(@env, md5_input, @sockaddr)
61
+ end
62
+ assert_equal @nr, reqs.size
63
+ t0 = Time.now
64
+ rv = HTTP_Spew.wait_mt reqs.size, reqs, 3600
65
+ elapsed = Time.now - t0
66
+ assert(elapsed <= 30, "took too long: #{elapsed}s")
67
+ assert_equal @nr, rv.size
68
+ rv.each { |r|
69
+ assert_kind_of HTTP_Spew::ChecksumError, r.error
70
+ }
71
+ assert_nil input.content_md5
72
+ assert_nil input.bytes_digested
73
+ end
74
+
75
+ def test_upload_with_valid_md5_sprayed_one_failure
76
+ str = rand_data(123) * (8 * 1021 * 13)
77
+ expect_md5 = [Digest::MD5.digest(str)].pack("m0")
78
+ @env["HTTP_CONTENT_MD5"] = expect_md5
79
+ expect = "expect=#{expect_md5}\nreaded=#{expect_md5}\n"
80
+ @env["CONTENT_LENGTH"] = str.size.to_s
81
+ @env["rack.input"] = StringIO.new(str)
82
+ input = HTTP_Spew::ContentMD5.new(@env)
83
+ sprayer = HTTP_Spew::InputSpray.new(@env, @nr, input)
84
+ assert_nil @env["CONTENT_LENGTH"]
85
+ assert_equal "chunked", @env["HTTP_TRANSFER_ENCODING"]
86
+ reqs = []
87
+ sprayer.readers.each_with_index do |md5_input,i|
88
+ env = @env.dup
89
+ env["HTTP_X_FAIL"] = "true" if i == 0
90
+ reqs << HTTP_Spew::Request.new(env, md5_input, @sockaddr)
91
+ end
92
+ assert_equal @nr, reqs.size
93
+ rv = HTTP_Spew.wait_mt reqs.size, reqs, 3600
94
+ assert_equal @nr, rv.size
95
+ rv.each { |resp| assert reqs.include?(resp) }
96
+ assert reqs[0].error
97
+ (1..3).each do |n|
98
+ resp = reqs[n]
99
+ assert_equal 200, resp.response[0].to_i
100
+ body = ""
101
+ resp.response[2].each { |buf| body << buf }
102
+ assert_equal expect, body
103
+ end
104
+ assert_equal expect_md5, input.content_md5
105
+ assert_equal 123 * 8 * 1021 * 13, input.bytes_digested
106
+ end
107
+ end
data/test/test_mirror.rb CHANGED
@@ -3,7 +3,7 @@ require "./test/helper"
3
3
 
4
4
  class TestMirror < Test::Unit::TestCase
5
5
  def setup
6
- @addr, @port, @srv = start_server("./test/mirror.ru")
6
+ @addr, @port, @srv = start_server("./test/mirror.ru", 4 , true)
7
7
  @sockaddr = Socket.pack_sockaddr_in(@port, @addr)
8
8
  @env = {
9
9
  "REQUEST_METHOD" => "PUT",
@@ -25,9 +25,9 @@ class TestMirror < Test::Unit::TestCase
25
25
  req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
26
26
  req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
27
27
  rv = HTTP_Spew.wait(3, req, 666000)
28
- rv.each do |req|
29
- assert_nil req.error
30
- response = req.response
28
+ rv.each do |r|
29
+ assert_nil r.error
30
+ response = r.response
31
31
  headers = Rack::Utils::HeaderHash.new(response[1])
32
32
  assert_equal 128, headers["Content-Length"].to_i
33
33
  assert_equal 200, response[0].to_i
@@ -46,9 +46,9 @@ class TestMirror < Test::Unit::TestCase
46
46
  req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
47
47
  req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
48
48
  rv = HTTP_Spew.wait(3, req, 6000)
49
- rv.each do |req|
50
- assert_nil req.error
51
- response = req.response
49
+ rv.each do |r|
50
+ assert_nil r.error
51
+ response = r.response
52
52
  headers = Rack::Utils::HeaderHash.new(response[1])
53
53
  assert_equal str.size, headers["Content-Length"].to_i
54
54
  assert_equal 200, response[0].to_i