unicorn 1.1.7 → 2.0.0pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/GIT-VERSION-GEN +1 -1
  2. data/GNUmakefile +14 -5
  3. data/Rakefile +3 -28
  4. data/TODO +7 -0
  5. data/bin/unicorn +9 -13
  6. data/bin/unicorn_rails +12 -14
  7. data/examples/big_app_gc.rb +33 -2
  8. data/ext/unicorn_http/global_variables.h +3 -1
  9. data/ext/unicorn_http/unicorn_http.rl +15 -6
  10. data/lib/unicorn.rb +67 -820
  11. data/lib/unicorn/app/exec_cgi.rb +3 -4
  12. data/lib/unicorn/configurator.rb +20 -25
  13. data/lib/unicorn/const.rb +26 -25
  14. data/lib/unicorn/http_request.rb +64 -57
  15. data/lib/unicorn/http_response.rb +16 -35
  16. data/lib/unicorn/http_server.rb +700 -0
  17. data/lib/unicorn/launcher.rb +4 -3
  18. data/lib/unicorn/oob_gc.rb +50 -61
  19. data/lib/unicorn/socket_helper.rb +4 -4
  20. data/lib/unicorn/tee_input.rb +18 -26
  21. data/lib/unicorn/tmpio.rb +29 -0
  22. data/lib/unicorn/util.rb +51 -85
  23. data/lib/unicorn/worker.rb +40 -0
  24. data/local.mk.sample +0 -9
  25. data/script/isolate_for_tests +43 -0
  26. data/t/GNUmakefile +8 -1
  27. data/t/t0003-working_directory.sh +0 -5
  28. data/t/t0010-reap-logging.sh +55 -0
  29. data/t/t0303-rails3-alt-working_directory_config.ru.sh +0 -5
  30. data/t/test-rails3.sh +1 -1
  31. data/test/exec/test_exec.rb +1 -1
  32. data/test/unit/test_http_parser_ng.rb +11 -0
  33. data/test/unit/test_request.rb +12 -0
  34. data/test/unit/test_response.rb +23 -21
  35. data/test/unit/test_signals.rb +1 -1
  36. data/test/unit/test_tee_input.rb +21 -19
  37. data/unicorn.gemspec +3 -2
  38. metadata +47 -25
  39. data/t/oob_gc.ru +0 -21
  40. data/t/oob_gc_path.ru +0 -21
  41. data/t/t0012-reload-empty-config.sh +0 -82
  42. data/t/t0018-write-on-close.sh +0 -23
  43. data/t/t9001-oob_gc.sh +0 -47
  44. data/t/t9002-oob_gc-path.sh +0 -75
  45. data/t/write-on-close.ru +0 -11
data/local.mk.sample CHANGED
@@ -37,15 +37,6 @@ else
37
37
  RUBY := $(prefix)/bin/ruby --disable-gems
38
38
  endif
39
39
 
40
- # FIXME: use isolate more
41
- ifndef RUBYLIB
42
- gems := rack-1.1.0
43
- gem_paths := $(addprefix $(HOME)/lib/ruby/gems/1.8/gems/,$(gems))
44
- sp :=
45
- sp +=
46
- export RUBYLIB := $(subst $(sp),:,$(addsuffix /lib,$(gem_paths)))
47
- endif
48
-
49
40
  # pipefail is THE reason to use bash (v3+) or never revisions of ksh93
50
41
  # SHELL := /bin/bash -e -o pipefail
51
42
  SHELL := /bin/ksh93 -e -o pipefail
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+ # scripts/Makefiles can read and eval the output of this script and
3
+ # use it as RUBYLIB
4
+ require 'rubygems'
5
+ require 'isolate'
6
+ fp = File.open(__FILE__, "rb")
7
+ fp.flock(File::LOCK_EX)
8
+
9
+ ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'
10
+ opts = {
11
+ :system => false,
12
+ # we want "ruby-1.8.7" and not "ruby-1.8", so disable :multiruby
13
+ :multiruby => false,
14
+ :path => "tmp/isolate/#{ruby_engine}-#{RUBY_VERSION}",
15
+ }
16
+
17
+ pid = fork do
18
+ Isolate.now!(opts) do
19
+ gem 'sqlite3-ruby', '1.2.5'
20
+ gem 'kgio', '1.2.0'
21
+ gem 'rack', '1.1.0'
22
+ end
23
+ end
24
+ _, status = Process.waitpid2(pid)
25
+ status.success? or abort status.inspect
26
+ lib_paths = Dir["#{opts[:path]}/gems/*-*/lib"].map { |x| File.expand_path(x) }
27
+ libs = "tmp/isolate/.#{ruby_engine}-#{RUBY_VERSION}.libs"
28
+ File.open("#{libs}.#$$", "w") { |fp| fp.puts lib_paths.join(':') }
29
+ File.rename("#{libs}.#$$", libs)
30
+
31
+ # pure Ruby gems can be shared across all Rubies
32
+ %w(3.0.0).each do |rails_ver|
33
+ opts[:path] = "tmp/isolate/rails-#{rails_ver}"
34
+ pid = fork do
35
+ Isolate.now!(opts) do
36
+ gem 'rails', rails_ver
37
+ end
38
+ end
39
+ _, status = Process.waitpid2(pid)
40
+ status.success? or abort status.inspect
41
+ more = Dir["#{opts[:path]}/gems/*-*/lib"].map { |x| File.expand_path(x) }
42
+ lib_paths.concat(more)
43
+ end
data/t/GNUmakefile CHANGED
@@ -17,6 +17,13 @@ endif
17
17
  RUBY_ENGINE := $(shell $(RUBY) -e 'puts((RUBY_ENGINE rescue "ruby"))')
18
18
  export RUBY_ENGINE
19
19
 
20
+ isolate_libs := ../tmp/isolate/.$(RUBY_ENGINE)-$(RUBY_VERSION).libs
21
+ MYLIBS := $(shell cat $(isolate_libs))
22
+ ifeq ($(MY_LIBS),)
23
+ ignore := $(shell cd .. && $(RUBY) ./script/isolate_for_tests)
24
+ MYLIBS := $(shell cat $(isolate_libs))
25
+ endif
26
+
20
27
  T = $(wildcard t[0-9][0-9][0-9][0-9]-*.sh)
21
28
 
22
29
  all:: $(T)
@@ -58,7 +65,7 @@ $(test_prefix)/.stamp:
58
65
  $(T): export RUBY := $(RUBY)
59
66
  $(T): export RAKE := $(RAKE)
60
67
  $(T): export PATH := $(test_prefix)/bin:$(PATH)
61
- $(T): export RUBYLIB := $(test_prefix)/lib:$(RUBYLIB)
68
+ $(T): export RUBYLIB := $(test_prefix)/lib:$(MYLIBS)
62
69
  $(T): dep $(test_prefix)/.stamp trash/.gitignore
63
70
  $(TRACER) $(SHELL) $(SH_TEST_OPTS) $@ $(TEST_OPTS)
64
71
 
@@ -1,9 +1,4 @@
1
1
  #!/bin/sh
2
- if test -n "$RBX_SKIP"
3
- then
4
- echo "$0 is broken under Rubinius for now"
5
- exit 0
6
- fi
7
2
  . ./test-lib.sh
8
3
 
9
4
  t_plan 4 "config.ru inside alt working_directory"
@@ -0,0 +1,55 @@
1
+ #!/bin/sh
2
+ . ./test-lib.sh
3
+ t_plan 9 "reap worker logging messages"
4
+
5
+ t_begin "setup and start" && {
6
+ unicorn_setup
7
+ cat >> $unicorn_config <<EOF
8
+ after_fork { |s,w| File.open('$fifo','w') { |f| f.write '.' } }
9
+ EOF
10
+ unicorn -c $unicorn_config pid.ru &
11
+ test '.' = $(cat $fifo)
12
+ unicorn_wait_start
13
+ }
14
+
15
+ t_begin "kill 1st worker=0" && {
16
+ pid_1=$(curl http://$listen/)
17
+ kill -9 $pid_1
18
+ }
19
+
20
+ t_begin "wait for 2nd worker to start" && {
21
+ test '.' = $(cat $fifo)
22
+ }
23
+
24
+ t_begin "ensure log of 1st reap is an ERROR" && {
25
+ dbgcat r_err
26
+ grep 'ERROR.*reaped.*worker=0' $r_err | grep $pid_1
27
+ dbgcat r_err
28
+ > $r_err
29
+ }
30
+
31
+ t_begin "kill 2nd worker gracefully" && {
32
+ pid_2=$(curl http://$listen/)
33
+ kill -QUIT $pid_2
34
+ }
35
+
36
+ t_begin "wait for 3rd worker=0 to start " && {
37
+ test '.' = $(cat $fifo)
38
+ }
39
+
40
+ t_begin "ensure log of 2nd reap is a INFO" && {
41
+ grep 'INFO.*reaped.*worker=0' $r_err | grep $pid_2
42
+ > $r_err
43
+ }
44
+
45
+ t_begin "killing succeeds" && {
46
+ kill $unicorn_pid
47
+ wait
48
+ kill -0 $unicorn_pid && false
49
+ }
50
+
51
+ t_begin "check stderr" && {
52
+ check_stderr
53
+ }
54
+
55
+ t_done
@@ -1,9 +1,4 @@
1
1
  #!/bin/sh
2
- if test -n "$RBX_SKIP"
3
- then
4
- echo "$0 is broken under Rubinius for now"
5
- exit 0
6
- fi
7
2
  . ./test-rails3.sh
8
3
 
9
4
  t_plan 5 "Rails 3 (beta) inside alt working_directory (w/ config.ru)"
data/t/test-rails3.sh CHANGED
@@ -13,7 +13,7 @@ rails_gems=../tmp/isolate/rails-$RAILS_VERSION/gems
13
13
  rails_bin="$rails_gems/rails-$RAILS_VERSION/bin/rails"
14
14
  if ! test -d "$arch_gems" || ! test -d "$rails_gems" || ! test -x "$rails_bin"
15
15
  then
16
- ( cd ../ && $RAKE isolate )
16
+ ( cd ../ && ./script/isolate_for_tests )
17
17
  fi
18
18
 
19
19
  for i in $arch_gems/*-* $rails_gems/*-*
@@ -614,7 +614,7 @@ EOF
614
614
  results = retry_hit(["http://#{@addr}:#{@port}/"])
615
615
  assert_equal String, results[0].class
616
616
  assert_shutdown(pid)
617
- end unless ENV['RBX_SKIP']
617
+ end
618
618
 
619
619
  def test_config_ru_alt_path
620
620
  config_path = "#{@tmpdir}/foo.ru"
@@ -440,6 +440,17 @@ class HttpParserNgTest < Test::Unit::TestCase
440
440
  end
441
441
  end
442
442
 
443
+ def test_backtrace_is_empty
444
+ begin
445
+ @parser.headers({}, "AAADFSFDSFD\r\n\r\n")
446
+ assert false, "should never get here line:#{__LINE__}"
447
+ rescue HttpParserError => e
448
+ assert_equal [], e.backtrace
449
+ return
450
+ end
451
+ assert false, "should never get here line:#{__LINE__}"
452
+ end
453
+
443
454
  def test_ignore_version_header
444
455
  http = "GET / HTTP/1.1\r\nVersion: hello\r\n\r\n"
445
456
  req = {}
@@ -11,7 +11,11 @@ class RequestTest < Test::Unit::TestCase
11
11
 
12
12
  class MockRequest < StringIO
13
13
  alias_method :readpartial, :sysread
14
+ alias_method :kgio_read!, :sysread
14
15
  alias_method :read_nonblock, :sysread
16
+ def kgio_addr
17
+ '127.0.0.1'
18
+ end
15
19
  end
16
20
 
17
21
  def setup
@@ -159,6 +163,14 @@ class RequestTest < Test::Unit::TestCase
159
163
  buf = (' ' * bs).freeze
160
164
  length = bs * count
161
165
  client = Tempfile.new('big_put')
166
+ def client.kgio_addr; '127.0.0.1'; end
167
+ def client.kgio_read(*args)
168
+ readpartial(*args)
169
+ rescue EOFError
170
+ end
171
+ def client.kgio_read!(*args)
172
+ readpartial(*args)
173
+ end
162
174
  client.syswrite(
163
175
  "PUT / HTTP/1.1\r\n" \
164
176
  "Host: foo\r\n" \
@@ -11,18 +11,20 @@ require 'test/test_helper'
11
11
  include Unicorn
12
12
 
13
13
  class ResponseTest < Test::Unit::TestCase
14
-
14
+ include Unicorn::HttpResponse
15
+
15
16
  def test_response_headers
16
17
  out = StringIO.new
17
- HttpResponse.write(out,[200, {"X-Whatever" => "stuff"}, ["cool"]])
18
- assert ! out.closed?
18
+ http_response_write(out,[200, {"X-Whatever" => "stuff"}, ["cool"]])
19
+ assert out.closed?
20
+
19
21
  assert out.length > 0, "output didn't have data"
20
22
  end
21
23
 
22
24
  def test_response_string_status
23
25
  out = StringIO.new
24
- HttpResponse.write(out,['200', {}, []])
25
- assert ! out.closed?
26
+ http_response_write(out,['200', {}, []])
27
+ assert out.closed?
26
28
  assert out.length > 0, "output didn't have data"
27
29
  assert_equal 1, out.string.split(/\r\n/).grep(/^Status: 200 OK/).size
28
30
  end
@@ -31,8 +33,8 @@ class ResponseTest < Test::Unit::TestCase
31
33
  old_ofs = $,
32
34
  $, = "\f\v"
33
35
  out = StringIO.new
34
- HttpResponse.write(out,[200, {"X-k" => "cd","X-y" => "z"}, ["cool"]])
35
- assert ! out.closed?
36
+ http_response_write(out,[200, {"X-k" => "cd","X-y" => "z"}, ["cool"]])
37
+ assert out.closed?
36
38
  resp = out.string
37
39
  assert ! resp.include?("\f\v"), "output didn't use $, ($OFS)"
38
40
  ensure
@@ -41,16 +43,16 @@ class ResponseTest < Test::Unit::TestCase
41
43
 
42
44
  def test_response_200
43
45
  io = StringIO.new
44
- HttpResponse.write(io, [200, {}, []])
45
- assert ! io.closed?
46
+ http_response_write(io, [200, {}, []])
47
+ assert io.closed?
46
48
  assert io.length > 0, "output didn't have data"
47
49
  end
48
50
 
49
51
  def test_response_with_default_reason
50
52
  code = 400
51
53
  io = StringIO.new
52
- HttpResponse.write(io, [code, {}, []])
53
- assert ! io.closed?
54
+ http_response_write(io, [code, {}, []])
55
+ assert io.closed?
54
56
  lines = io.string.split(/\r\n/)
55
57
  assert_match(/.* Bad Request$/, lines.first,
56
58
  "wrong default reason phrase")
@@ -58,8 +60,8 @@ class ResponseTest < Test::Unit::TestCase
58
60
 
59
61
  def test_rack_multivalue_headers
60
62
  out = StringIO.new
61
- HttpResponse.write(out,[200, {"X-Whatever" => "stuff\nbleh"}, []])
62
- assert ! out.closed?
63
+ http_response_write(out,[200, {"X-Whatever" => "stuff\nbleh"}, []])
64
+ assert out.closed?
63
65
  assert_match(/^X-Whatever: stuff\r\nX-Whatever: bleh\r\n/, out.string)
64
66
  end
65
67
 
@@ -67,8 +69,8 @@ class ResponseTest < Test::Unit::TestCase
67
69
  # some broken clients still rely on it
68
70
  def test_status_header_added
69
71
  out = StringIO.new
70
- HttpResponse.write(out,[200, {"X-Whatever" => "stuff"}, []])
71
- assert ! out.closed?
72
+ http_response_write(out,[200, {"X-Whatever" => "stuff"}, []])
73
+ assert out.closed?
72
74
  assert_equal 1, out.string.split(/\r\n/).grep(/^Status: 200 OK/i).size
73
75
  end
74
76
 
@@ -78,8 +80,8 @@ class ResponseTest < Test::Unit::TestCase
78
80
  def test_status_header_ignores_app_hash
79
81
  out = StringIO.new
80
82
  header_hash = {"X-Whatever" => "stuff", 'StaTus' => "666" }
81
- HttpResponse.write(out,[200, header_hash, []])
82
- assert ! out.closed?
83
+ http_response_write(out,[200, header_hash, []])
84
+ assert out.closed?
83
85
  assert_equal 1, out.string.split(/\r\n/).grep(/^Status: 200 OK/i).size
84
86
  assert_equal 1, out.string.split(/\r\n/).grep(/^Status:/i).size
85
87
  end
@@ -89,16 +91,16 @@ class ResponseTest < Test::Unit::TestCase
89
91
  body = StringIO.new(expect_body)
90
92
  body.rewind
91
93
  out = StringIO.new
92
- HttpResponse.write(out,[200, {}, body])
93
- assert ! out.closed?
94
+ http_response_write(out,[200, {}, body])
95
+ assert out.closed?
94
96
  assert body.closed?
95
97
  assert_match(expect_body, out.string.split(/\r\n/).last)
96
98
  end
97
99
 
98
100
  def test_unknown_status_pass_through
99
101
  out = StringIO.new
100
- HttpResponse.write(out,["666 I AM THE BEAST", {}, [] ])
101
- assert ! out.closed?
102
+ http_response_write(out,["666 I AM THE BEAST", {}, [] ])
103
+ assert out.closed?
102
104
  headers = out.string.split(/\r\n\r\n/).first.split(/\r\n/)
103
105
  assert %r{\AHTTP/\d\.\d 666 I AM THE BEAST\z}.match(headers[0])
104
106
  status = headers.grep(/\AStatus:/i).first
@@ -166,7 +166,7 @@ class SignalsTest < Test::Unit::TestCase
166
166
  expect = @bs * @count
167
167
  assert_equal(expect, got, "expect=#{expect} got=#{got}")
168
168
  assert_nothing_raised { sock.close }
169
- end unless ENV['RBX_SKIP']
169
+ end
170
170
 
171
171
  def test_request_read
172
172
  app = lambda { |env|
@@ -5,11 +5,12 @@ require 'digest/sha1'
5
5
  require 'unicorn'
6
6
 
7
7
  class TestTeeInput < Test::Unit::TestCase
8
+ MockRequest = Struct.new(:env, :parser, :buf)
8
9
 
9
10
  def setup
10
11
  @rs = $/
11
12
  @env = {}
12
- @rd, @wr = IO.pipe
13
+ @rd, @wr = Kgio::UNIXSocket.pair
13
14
  @rd.sync = @wr.sync = true
14
15
  @start_pid = $$
15
16
  end
@@ -27,8 +28,8 @@ class TestTeeInput < Test::Unit::TestCase
27
28
  end
28
29
 
29
30
  def test_gets_long
30
- init_parser("hello", 5 + (4096 * 4 * 3) + "#$/foo#$/".size)
31
- ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
31
+ r = init_request("hello", 5 + (4096 * 4 * 3) + "#$/foo#$/".size)
32
+ ti = Unicorn::TeeInput.new(@rd, r)
32
33
  status = line = nil
33
34
  pid = fork {
34
35
  @rd.close
@@ -48,8 +49,8 @@ class TestTeeInput < Test::Unit::TestCase
48
49
  end
49
50
 
50
51
  def test_gets_short
51
- init_parser("hello", 5 + "#$/foo".size)
52
- ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
52
+ r = init_request("hello", 5 + "#$/foo".size)
53
+ ti = Unicorn::TeeInput.new(@rd, r)
53
54
  status = line = nil
54
55
  pid = fork {
55
56
  @rd.close
@@ -67,8 +68,8 @@ class TestTeeInput < Test::Unit::TestCase
67
68
  end
68
69
 
69
70
  def test_small_body
70
- init_parser('hello')
71
- ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
71
+ r = init_request('hello')
72
+ ti = Unicorn::TeeInput.new(@rd, r)
72
73
  assert_equal 0, @parser.content_length
73
74
  assert @parser.body_eof?
74
75
  assert_equal StringIO, ti.tmp.class
@@ -80,8 +81,8 @@ class TestTeeInput < Test::Unit::TestCase
80
81
  end
81
82
 
82
83
  def test_read_with_buffer
83
- init_parser('hello')
84
- ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
84
+ r = init_request('hello')
85
+ ti = Unicorn::TeeInput.new(@rd, r)
85
86
  buf = ''
86
87
  rv = ti.read(4, buf)
87
88
  assert_equal 'hell', rv
@@ -95,8 +96,8 @@ class TestTeeInput < Test::Unit::TestCase
95
96
  end
96
97
 
97
98
  def test_big_body
98
- init_parser('.' * Unicorn::Const::MAX_BODY << 'a')
99
- ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
99
+ r = init_request('.' * Unicorn::Const::MAX_BODY << 'a')
100
+ ti = Unicorn::TeeInput.new(@rd, r)
100
101
  assert_equal 0, @parser.content_length
101
102
  assert @parser.body_eof?
102
103
  assert_kind_of File, ti.tmp
@@ -106,9 +107,9 @@ class TestTeeInput < Test::Unit::TestCase
106
107
 
107
108
  def test_read_in_full_if_content_length
108
109
  a, b = 300, 3
109
- init_parser('.' * b, 300)
110
+ r = init_request('.' * b, 300)
110
111
  assert_equal 300, @parser.content_length
111
- ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
112
+ ti = Unicorn::TeeInput.new(@rd, r)
112
113
  pid = fork {
113
114
  @wr.write('.' * 197)
114
115
  sleep 1 # still a *potential* race here that would make the test moot...
@@ -121,8 +122,8 @@ class TestTeeInput < Test::Unit::TestCase
121
122
  end
122
123
 
123
124
  def test_big_body_multi
124
- init_parser('.', Unicorn::Const::MAX_BODY + 1)
125
- ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
125
+ r = init_request('.', Unicorn::Const::MAX_BODY + 1)
126
+ ti = Unicorn::TeeInput.new(@rd, r)
126
127
  assert_equal Unicorn::Const::MAX_BODY, @parser.content_length
127
128
  assert ! @parser.body_eof?
128
129
  assert_kind_of File, ti.tmp
@@ -163,7 +164,7 @@ class TestTeeInput < Test::Unit::TestCase
163
164
  @wr.write("0\r\n\r\n")
164
165
  }
165
166
  @wr.close
166
- ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
167
+ ti = Unicorn::TeeInput.new(@rd, MockRequest.new(@env, @parser, @buf))
167
168
  assert_nil @parser.content_length
168
169
  assert_nil ti.len
169
170
  assert ! @parser.body_eof?
@@ -201,7 +202,7 @@ class TestTeeInput < Test::Unit::TestCase
201
202
  end
202
203
  @wr.write("0\r\n\r\n")
203
204
  }
204
- ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
205
+ ti = Unicorn::TeeInput.new(@rd, MockRequest.new(@env, @parser, @buf))
205
206
  assert_nil @parser.content_length
206
207
  assert_nil ti.len
207
208
  assert ! @parser.body_eof?
@@ -230,7 +231,7 @@ class TestTeeInput < Test::Unit::TestCase
230
231
  @wr.write("Hello: World\r\n\r\n")
231
232
  }
232
233
  @wr.close
233
- ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf)
234
+ ti = Unicorn::TeeInput.new(@rd, MockRequest.new(@env, @parser, @buf))
234
235
  assert_nil @parser.content_length
235
236
  assert_nil ti.len
236
237
  assert ! @parser.body_eof?
@@ -243,7 +244,7 @@ class TestTeeInput < Test::Unit::TestCase
243
244
 
244
245
  private
245
246
 
246
- def init_parser(body, size = nil)
247
+ def init_request(body, size = nil)
247
248
  @parser = Unicorn::HttpParser.new
248
249
  body = body.to_s.freeze
249
250
  @buf = "POST / HTTP/1.1\r\n" \
@@ -252,6 +253,7 @@ private
252
253
  "\r\n#{body}"
253
254
  assert_equal @env, @parser.headers(@env, @buf)
254
255
  assert_equal body, @buf
256
+ MockRequest.new(@env, @parser, @buf)
255
257
  end
256
258
 
257
259
  end