unicorn 1.1.7 → 2.0.0pre1

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.
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