http_spew 0.1.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,21 @@
1
+ # SHA1 checksum generator
2
+ bs = ENV['bs'] ? ENV['bs'].to_i : 4096
3
+ require 'digest/md5'
4
+ use Rack::ContentLength
5
+ app = lambda do |env|
6
+ digest = Digest::MD5.new
7
+ input = env['rack.input']
8
+ if buf = input.read(bs)
9
+ begin
10
+ digest.update(buf)
11
+ end while input.read(bs, buf)
12
+ end
13
+
14
+ expect = env['HTTP_CONTENT_MD5']
15
+ readed = [ digest.digest ].pack('m').strip
16
+ body = "expect=#{expect}\nreaded=#{readed}\n"
17
+ status = expect == readed ? 200 : 500
18
+
19
+ [ status, {'Content-Type' => 'text/plain'}, [ body ] ]
20
+ end
21
+ run app
data/test/helper.rb ADDED
@@ -0,0 +1,41 @@
1
+ # -*- encoding: binary -*-
2
+ $stderr.sync = $stdout.sync = true
3
+ require "test/unit"
4
+ require "digest/sha1"
5
+ require "stringio"
6
+ require "tmpdir"
7
+ require "socket"
8
+ require "rack"
9
+ require "tempfile"
10
+ $-w = true
11
+ require "http_spew"
12
+
13
+ def start_server(config, worker_processes = 4)
14
+ addr = ENV["TEST_HOST"] || "127.0.0.1"
15
+ sock = TCPServer.new(addr, 0)
16
+ port = sock.addr[1]
17
+ fifo = Tempfile.new("fifo")
18
+ fifo_path = fifo.path
19
+ fifo.close!
20
+ system("mkfifo", fifo_path) or abort "mkfifo: #$?"
21
+ cfg = Tempfile.new("unicorn.config")
22
+ cfg.puts "worker_processes #{worker_processes}"
23
+ cfg.puts "preload_app true"
24
+ cfg.puts <<EOF
25
+ after_fork do |s,w|
26
+ w.nr == (s.worker_processes - 1) and File.open("#{fifo_path}", "w").close
27
+ end
28
+ EOF
29
+ cfg.flush
30
+ pid = fork do
31
+ ENV["UNICORN_FD"] = sock.fileno.to_s
32
+ exec "unicorn", "-l#{addr}:#{port}", "-c#{cfg.path}", config
33
+ end
34
+ File.open(fifo_path).close
35
+ File.unlink fifo_path
36
+ [ addr, port, pid ]
37
+ end
38
+
39
+ def rand_data(nr)
40
+ File.open("/dev/urandom", "rb") { |fp| fp.read(nr) }
41
+ end
data/test/mirror.ru ADDED
@@ -0,0 +1,22 @@
1
+ # SHA1 checksum generator
2
+ bs = ENV['bs'] ? ENV['bs'].to_i : 16384
3
+ require 'digest/sha1'
4
+ require 'unicorn/preread_input'
5
+ use Unicorn::PrereadInput
6
+ class InputWrap < Struct.new(:input)
7
+ def each
8
+ buf = ""
9
+ while buf = input.read(0x4000, buf)
10
+ yield buf
11
+ end
12
+ end
13
+ end
14
+
15
+ app = lambda do |env|
16
+ headers = {
17
+ "Content-Type" => "application/octet-stream",
18
+ "Content-Length" => env["CONTENT_LENGTH"].to_s,
19
+ }
20
+ [ 200, headers, InputWrap.new(env["rack.input"]) ]
21
+ end
22
+ run app
data/test/sha1.ru ADDED
@@ -0,0 +1,18 @@
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
+
16
+ [ 200, {'Content-Type' => 'text/plain'}, [ digest.hexdigest << "\n" ] ]
17
+ end
18
+ run app
@@ -0,0 +1,85 @@
1
+ # -*- encoding: binary -*-
2
+ require "./test/helper"
3
+
4
+ class TestContentMD5 < Test::Unit::TestCase
5
+ def setup
6
+ ENV["RACK_ENV"] = "deployment" # quiet Rack 1.2.1 bug
7
+ @addr, @port, @srv = start_server("./test/content-md5.ru", 1)
8
+ @sockaddr = Socket.pack_sockaddr_in(@port, @addr)
9
+ @env = {
10
+ "REQUEST_METHOD" => "PUT",
11
+ "REQUEST_URI" => "/",
12
+ "HTTP_HOST" => "example.com",
13
+ }
14
+ @tmpfiles = []
15
+ end
16
+
17
+ def teardown
18
+ Process.kill(:QUIT, @srv)
19
+ Process.waitpid2(@srv)
20
+ @tmpfiles.each { |tmp| tmp.closed? or tmp.close! }
21
+ end
22
+
23
+ def test_upload_with_md5
24
+ str = rand_data(123) * (8 * 1021 * 13)
25
+ expect = [Digest::MD5.digest(str)].pack("m").strip!
26
+ expect = "expect=#{expect}\nreaded=#{expect}\n"
27
+ @env["CONTENT_LENGTH"] = str.size.to_s
28
+ @env["rack.input"] = StringIO.new(str)
29
+ input = HTTP_Spew::ContentMD5.input(@env)
30
+ assert_nil @env["CONTENT_LENGTH"]
31
+ assert_equal "chunked", @env["HTTP_TRANSFER_ENCODING"]
32
+ req = HTTP_Spew::Request.new(@env, input, @sockaddr)
33
+ rv = HTTP_Spew.wait 1, [req], 666000
34
+ assert_equal 200, rv[0].response[0].to_i
35
+ body = ""
36
+ req.each { |buf| body << buf }
37
+ assert_equal body, expect
38
+ end
39
+
40
+ def test_upload_with_corrupt_md5
41
+ str = rand_data(123) * (8 * 1021 * 13)
42
+ expect = [Digest::MD5.digest(str)].pack("m").strip!
43
+ @env["HTTP_CONTENT_MD5"] = expect
44
+ str = rand_data(123) * (8 * 1021 * 13)
45
+ @env["CONTENT_LENGTH"] = str.size.to_s
46
+ @env["rack.input"] = StringIO.new(str)
47
+ input = HTTP_Spew::ContentMD5.input(@env)
48
+ assert_nil @env["CONTENT_LENGTH"]
49
+ assert_equal "chunked", @env["HTTP_TRANSFER_ENCODING"]
50
+ 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
53
+ end
54
+
55
+ def test_upload_with_corrupt_length
56
+ str = rand_data(123) * (8 * 1021 * 13)
57
+ expect = [Digest::MD5.digest(str)].pack("m").strip!
58
+ @env["HTTP_CONTENT_MD5"] = expect
59
+ str = rand_data(123) * (8 * 1021 * 13)
60
+ @env["CONTENT_LENGTH"] = (str.size + 1).to_s
61
+ @env["rack.input"] = StringIO.new(str)
62
+ input = HTTP_Spew::ContentMD5.input(@env)
63
+ assert_nil @env["CONTENT_LENGTH"]
64
+ assert_equal "chunked", @env["HTTP_TRANSFER_ENCODING"]
65
+ 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
68
+ end
69
+
70
+ def test_upload_with_valid_md5
71
+ str = rand_data(123) * (8 * 1021 * 13)
72
+ expect = [Digest::MD5.digest(str)].pack("m").strip!
73
+ @env["HTTP_CONTENT_MD5"] = expect
74
+ @env["CONTENT_LENGTH"] = str.size.to_s
75
+ @env["rack.input"] = StringIO.new(str)
76
+ input = HTTP_Spew::ContentMD5.input(@env)
77
+ assert_nil @env["CONTENT_LENGTH"]
78
+ assert_equal "chunked", @env["HTTP_TRANSFER_ENCODING"]
79
+ req = HTTP_Spew::Request.new(@env, input, @sockaddr)
80
+ rv = HTTP_Spew.wait 1, [req], 3600_000
81
+ assert_equal 200, rv[0].response[0].to_i
82
+ body = ""
83
+ req.each { |buf| body << buf }
84
+ end
85
+ end
@@ -0,0 +1,47 @@
1
+ require "./test/helper"
2
+
3
+ class TestHitNRun < Test::Unit::TestCase
4
+ def setup
5
+ @addr, @port, @srv = start_server("./test/sha1.ru", 1)
6
+ @sockaddr = Socket.pack_sockaddr_in(@port, @addr)
7
+ @env = {
8
+ "REQUEST_METHOD" => "PUT",
9
+ "REQUEST_URI" => "/",
10
+ "HTTP_HOST" => "example.com",
11
+ }
12
+ @tmpfiles = []
13
+ end
14
+
15
+ def teardown
16
+ Process.kill(:QUIT, @srv)
17
+ Process.waitpid2(@srv)
18
+ @tmpfiles.each { |tmp| tmp.closed? or tmp.close! }
19
+ end
20
+
21
+ def test_request_with_existing_socket
22
+ sock = Kgio::Socket.new(@sockaddr)
23
+ req = HTTP_Spew::HitNRun.new(@env, nil, sock)
24
+ assert_equal sock, req.to_io
25
+ assert_nothing_raised { req.close }
26
+ assert sock.closed?
27
+ end
28
+
29
+ def test_request_single
30
+ req = HTTP_Spew::HitNRun.new(@env, nil, @sockaddr)
31
+ sym = req.resume
32
+ if sym == :wait_writable
33
+ set = Kgio.poll({req => sym}, 100)
34
+ assert_equal [ req ], set.keys
35
+ sym = req.resume
36
+ end
37
+ assert_equal HTTP_Spew::HitNRun::RESPONSE.object_id, sym.object_id
38
+ end
39
+
40
+ def test_request_loop
41
+ req = HTTP_Spew::HitNRun.new(@env, nil, @sockaddr)
42
+ until Array === (rv = req.resume)
43
+ Kgio.poll(req => rv)
44
+ end
45
+ assert_equal HTTP_Spew::HitNRun::RESPONSE.object_id, rv.object_id
46
+ end
47
+ end
@@ -0,0 +1,101 @@
1
+ # -*- encoding: binary -*-
2
+ require "./test/helper"
3
+
4
+ class TestInputSpray < Test::Unit::TestCase
5
+ BUF = (rand_data(128) * 1024 * 8 * 4).freeze
6
+ EXPECT = Digest::SHA1.hexdigest(BUF).freeze
7
+
8
+ def setup
9
+ io = StringIO.new(BUF)
10
+ @env = {
11
+ "rack.input" => io,
12
+ "CONTENT_LENGTH" => io.size.to_s
13
+ }
14
+ end
15
+
16
+ def test_spray_ok
17
+ sprayer = HTTP_Spew::InputSpray.new(@env, 2)
18
+ readers = sprayer.readers
19
+ assert_equal 2, readers.size
20
+
21
+ sha1 = Hash[readers.map { |rd| [ rd, Digest::SHA1.new ] } ]
22
+
23
+ readers.delete_if do |rd|
24
+ if buf = rd.read(0x10000)
25
+ sha1[rd].update buf
26
+ false
27
+ else
28
+ rd.close.nil?
29
+ end
30
+ end until readers.empty?
31
+
32
+ sha1.each_value { |dig| assert_equal EXPECT, dig.hexdigest }
33
+ sprayer.instance_variable_get(:@writers).each { |x| assert x.closed? }
34
+ end
35
+
36
+ def test_spray_one_reader_dies
37
+ sprayer = HTTP_Spew::InputSpray.new(@env, 3)
38
+ readers = sprayer.readers
39
+ assert_equal 3, readers.size
40
+ sha1 = Hash[readers.map { |rd| [ rd, Digest::SHA1.new ] } ]
41
+ count = Hash[readers.map { |rd| [ rd, 0 ] } ]
42
+ to_die = readers[0]
43
+ readers.delete_if do |rd|
44
+ if buf = rd.read(0x100)
45
+ sha1[rd].update buf
46
+ n = count[rd] += buf.size
47
+ if rd == to_die && n >= 0x20000
48
+ rd.close.nil?
49
+ else
50
+ false
51
+ end
52
+ else
53
+ rd.close.nil?
54
+ end
55
+ end until readers.empty?
56
+
57
+ dead_sha1 = sha1.delete to_die
58
+ assert EXPECT != dead_sha1.hexdigest
59
+ sha1.each_value { |dig| assert_equal EXPECT, dig.hexdigest }
60
+ sprayer.instance_variable_get(:@writers).each { |x| assert x.closed? }
61
+ end
62
+
63
+ def test_spray_stream
64
+ rd, wr = IO.pipe
65
+ assert @env.delete("CONTENT_LENGTH")
66
+ io = @env.delete("rack.input")
67
+ buf = ""
68
+ thr = Thread.new do
69
+ while buf = io.read(128, buf)
70
+ wr.write buf
71
+ end
72
+ wr.close
73
+ :ok
74
+ end
75
+ @env["rack.input"] = rd
76
+ sprayer = HTTP_Spew::InputSpray.new(@env, 2)
77
+ readers = sprayer.readers
78
+ assert_equal 2, readers.size
79
+
80
+ sha1 = Hash[readers.map { |rd| [ rd, Digest::SHA1.new ] } ]
81
+
82
+ readers.delete_if do |rd|
83
+ assert ! rd.respond_to?(:size)
84
+ if buf = rd.read(0x10000)
85
+ sha1[rd].update buf
86
+ false
87
+ else
88
+ rd.close.nil?
89
+ end
90
+ end until readers.empty?
91
+
92
+ sha1.each_value { |dig| assert_equal EXPECT, dig.hexdigest }
93
+ sprayer.instance_variable_get(:@writers).each { |x| assert x.closed? }
94
+ thr.join
95
+ assert_equal :ok, thr.value
96
+ assert ! rd.closed?
97
+ assert wr.closed?
98
+ ensure
99
+ rd.close
100
+ end
101
+ end
@@ -0,0 +1,60 @@
1
+ # -*- encoding: binary -*-
2
+ require "./test/helper"
3
+
4
+ class TestMirror < Test::Unit::TestCase
5
+ def setup
6
+ @addr, @port, @srv = start_server("./test/mirror.ru")
7
+ @sockaddr = Socket.pack_sockaddr_in(@port, @addr)
8
+ @env = {
9
+ "REQUEST_METHOD" => "PUT",
10
+ "REQUEST_URI" => "/",
11
+ "HTTP_HOST" => "example.com",
12
+ }
13
+ end
14
+
15
+ def teardown
16
+ Process.kill(:QUIT, @srv)
17
+ Process.waitpid2(@srv)
18
+ end
19
+
20
+ def test_mirror_small
21
+ str = rand_data(128)
22
+ expect = [ str ]
23
+ req = []
24
+ req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
25
+ req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
26
+ req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
27
+ rv = HTTP_Spew.wait(3, req, 666000)
28
+ rv.each do |req|
29
+ assert_nil req.error
30
+ response = req.response
31
+ headers = Rack::Utils::HeaderHash.new(response[1])
32
+ assert_equal 128, headers["Content-Length"].to_i
33
+ assert_equal 200, response[0].to_i
34
+ tmp = []
35
+ response[2].each { |x| tmp << x.dup }
36
+ assert_equal expect, tmp
37
+ end
38
+ end
39
+
40
+ # no bidirectional input support
41
+ def test_mirror_big
42
+ str = rand_data(128) * (8 * 1024 * 8)
43
+ expect = str
44
+ req = []
45
+ req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
46
+ req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
47
+ req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
48
+ rv = HTTP_Spew.wait(3, req, 6000)
49
+ rv.each do |req|
50
+ assert_nil req.error
51
+ response = req.response
52
+ headers = Rack::Utils::HeaderHash.new(response[1])
53
+ assert_equal str.size, headers["Content-Length"].to_i
54
+ assert_equal 200, response[0].to_i
55
+ tmp = ""
56
+ response[2].each { |x| tmp << x }
57
+ assert_equal expect, tmp
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,54 @@
1
+ require "./test/helper"
2
+
3
+ class TestRequest < Test::Unit::TestCase
4
+ def setup
5
+ @addr, @port, @srv = start_server("./test/sha1.ru", 1)
6
+ @sockaddr = Socket.pack_sockaddr_in(@port, @addr)
7
+ @env = {
8
+ "REQUEST_METHOD" => "PUT",
9
+ "REQUEST_URI" => "/",
10
+ "HTTP_HOST" => "example.com",
11
+ }
12
+ @tmpfiles = []
13
+ end
14
+
15
+ def teardown
16
+ Process.kill(:QUIT, @srv)
17
+ Process.waitpid2(@srv)
18
+ @tmpfiles.each { |tmp| tmp.closed? or tmp.close! }
19
+ end
20
+
21
+ def test_request_with_existing_socket
22
+ sock = Kgio::Socket.new(@sockaddr)
23
+ req = HTTP_Spew::Request.new(@env, nil, sock)
24
+ assert_equal sock, req.to_io
25
+ assert_nothing_raised { req.close }
26
+ assert sock.closed?
27
+ end
28
+
29
+ def test_request_single
30
+ req = HTTP_Spew::Request.new(@env, nil, @sockaddr)
31
+ sym = req.resume
32
+ assert_kind_of(Symbol, sym)
33
+ if sym == :wait_writable
34
+ set = Kgio.poll({req => sym}, 100)
35
+ assert_equal [ req ], set.keys
36
+ sym = req.resume
37
+ end
38
+ assert_equal :wait_readable, sym
39
+ set = Kgio.poll({req => sym}, 100)
40
+ assert_equal [ req ], set.keys
41
+ rv = req.resume
42
+ assert_equal req, rv[2]
43
+ end
44
+
45
+ def test_request_loop
46
+ req = HTTP_Spew::Request.new(@env, nil, @sockaddr)
47
+ until Array === (rv = req.resume)
48
+ Kgio.poll(req => rv)
49
+ end
50
+ assert_kind_of Array, rv
51
+ assert_equal 3, rv.size
52
+ assert_equal req, rv[2]
53
+ end
54
+ end
@@ -0,0 +1,127 @@
1
+ # -*- encoding: binary -*-
2
+ require "./test/helper"
3
+
4
+ class TestUpload < Test::Unit::TestCase
5
+ def setup
6
+ @addr, @port, @srv = start_server("./test/sha1.ru")
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_spew_upload_empty
23
+ req = []
24
+ req << HTTP_Spew::Request.new(@env, nil, @sockaddr)
25
+ req << HTTP_Spew::Request.new(@env, nil, @sockaddr)
26
+ req << HTTP_Spew::Request.new(@env, nil, @sockaddr)
27
+ rv = HTTP_Spew.wait(3, req, 666000)
28
+ assert_equal 3, rv.size
29
+ rv.each do |req|
30
+ assert_nil req.error
31
+ response = req.response
32
+ assert_equal 200, response[0].to_i
33
+ tmp = []
34
+ response[2].each { |x| tmp << x.dup }
35
+ assert_equal [ "da39a3ee5e6b4b0d3255bfef95601890afd80709\n" ], tmp
36
+ end
37
+ end
38
+
39
+ def tmp_blob(str)
40
+ tmp = Tempfile.new "blob"
41
+ tmp.write str
42
+ tmp.flush
43
+ assert_equal str.size, tmp.size
44
+ tmp.rewind
45
+ @tmpfiles << tmp
46
+ tmp
47
+ end
48
+
49
+ def test_spew_upload_big
50
+ str = rand_data(128) * (8 * 1024 * 4)
51
+ expect = [ Digest::SHA1.hexdigest(str) << "\n" ]
52
+ req = []
53
+ req << HTTP_Spew::Request.new(@env, tmp_blob(str), @sockaddr)
54
+ req << HTTP_Spew::Request.new(@env, tmp_blob(str), @sockaddr)
55
+ req << HTTP_Spew::Request.new(@env, tmp_blob(str), @sockaddr)
56
+ rv = HTTP_Spew.wait(3, req, 666000)
57
+ assert_equal 3, rv.size
58
+ rv.each do |req|
59
+ assert_nil req.error
60
+ response = req.response
61
+ assert_equal 200, response[0].to_i
62
+ tmp = []
63
+ response[2].each { |x| tmp << x.dup }
64
+ assert_equal expect, tmp
65
+ end
66
+ end
67
+
68
+ def test_spew_upload_small
69
+ str = rand_data(128)
70
+ expect = [ Digest::SHA1.hexdigest(str) << "\n" ]
71
+ req = []
72
+ req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
73
+ req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
74
+ req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
75
+ rv = HTTP_Spew.wait(3, req, 666000)
76
+ assert_equal 3, rv.size
77
+ rv.each do |req|
78
+ assert_nil req.error
79
+ response = req.response
80
+ assert_equal 200, response[0].to_i
81
+ tmp = []
82
+ response[2].each { |x| tmp << x.dup }
83
+ assert_equal expect, tmp
84
+ end
85
+ end
86
+
87
+ def test_spew_upload_small_two_of_three
88
+ str = rand_data(128)
89
+ expect = [ Digest::SHA1.hexdigest(str) << "\n" ]
90
+ req = []
91
+ req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
92
+ req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
93
+ req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
94
+ rv = HTTP_Spew.wait(2, req, 666000)
95
+ assert_equal 3, rv.size
96
+ rv[0, 2].each do |req|
97
+ assert_nil req.error
98
+ response = req.response
99
+ assert_equal 200, response[0].to_i
100
+ tmp = []
101
+ response[2].each { |x| tmp << x.dup }
102
+ assert_equal expect, tmp
103
+ assert_nothing_raised { response[2].close }
104
+ assert req.to_io.closed?
105
+ end
106
+ failed = rv[2]
107
+ assert_kind_of(HTTP_Spew::ConnectionReset, failed.error)
108
+ assert failed.to_io.closed?
109
+ end
110
+
111
+ def test_spew_upload_nonblock
112
+ str = rand_data(128)
113
+ req = []
114
+ req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
115
+ req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
116
+ req << HTTP_Spew::Request.new(@env, StringIO.new(str), @sockaddr)
117
+ before = req.dup
118
+ rv = HTTP_Spew.wait_nonblock!(3, req)
119
+ assert_nil rv
120
+ while rv.nil? do
121
+ rv = HTTP_Spew.wait_nonblock!(3, req)
122
+ end
123
+ assert_nil rv.uniq!
124
+ assert rv.size > 0
125
+ rv.each { |req| assert before.include?(req) }
126
+ end
127
+ end