unicorn 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.
- data/.document +12 -0
- data/.gitignore +12 -0
- data/CHANGELOG +19 -0
- data/CONTRIBUTORS +20 -0
- data/DESIGN +80 -0
- data/GNUmakefile +111 -0
- data/LICENSE +53 -0
- data/Manifest +41 -0
- data/README +73 -0
- data/Rakefile +37 -0
- data/SIGNALS +34 -0
- data/TODO +5 -0
- data/bin/unicorn +189 -0
- data/ext/unicorn/http11/ext_help.h +15 -0
- data/ext/unicorn/http11/extconf.rb +5 -0
- data/ext/unicorn/http11/http11.c +526 -0
- data/ext/unicorn/http11/http11_parser.c +1220 -0
- data/ext/unicorn/http11/http11_parser.h +45 -0
- data/ext/unicorn/http11/http11_parser.rl +153 -0
- data/ext/unicorn/http11/http11_parser_common.rl +55 -0
- data/lib/unicorn.rb +548 -0
- data/lib/unicorn/configurator.rb +253 -0
- data/lib/unicorn/const.rb +116 -0
- data/lib/unicorn/http_request.rb +178 -0
- data/lib/unicorn/http_response.rb +72 -0
- data/lib/unicorn/socket.rb +142 -0
- data/lib/unicorn/util.rb +40 -0
- data/setup.rb +1585 -0
- data/test/aggregate.rb +13 -0
- data/test/benchmark/previous.rb +11 -0
- data/test/benchmark/simple.rb +11 -0
- data/test/benchmark/utils.rb +82 -0
- data/test/exec/README +5 -0
- data/test/exec/test_exec.rb +570 -0
- data/test/test_helper.rb +103 -0
- data/test/tools/trickletest.rb +45 -0
- data/test/unit/test_configurator.rb +48 -0
- data/test/unit/test_http_parser.rb +161 -0
- data/test/unit/test_response.rb +45 -0
- data/test/unit/test_server.rb +96 -0
- data/test/unit/test_upload.rb +151 -0
- data/unicorn.gemspec +35 -0
- metadata +122 -0
data/test/test_helper.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# Copyright (c) 2005 Zed A. Shaw
|
2
|
+
# You can redistribute it and/or modify it under the same terms as Ruby.
|
3
|
+
#
|
4
|
+
# Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html
|
5
|
+
# for more information.
|
6
|
+
|
7
|
+
|
8
|
+
HERE = File.dirname(__FILE__) unless defined?(HERE)
|
9
|
+
%w(lib ext bin test).each do |dir|
|
10
|
+
$LOAD_PATH.unshift "#{HERE}/../#{dir}"
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'test/unit'
|
14
|
+
require 'net/http'
|
15
|
+
require 'digest/sha1'
|
16
|
+
require 'uri'
|
17
|
+
require 'stringio'
|
18
|
+
require 'unicorn'
|
19
|
+
require 'tmpdir'
|
20
|
+
|
21
|
+
if ENV['DEBUG']
|
22
|
+
require 'ruby-debug'
|
23
|
+
Debugger.start
|
24
|
+
end
|
25
|
+
|
26
|
+
def redirect_test_io
|
27
|
+
orig_err = STDERR.dup
|
28
|
+
orig_out = STDOUT.dup
|
29
|
+
STDERR.reopen("test_stderr.#{$$}.log")
|
30
|
+
STDOUT.reopen("test_stdout.#{$$}.log")
|
31
|
+
|
32
|
+
at_exit do
|
33
|
+
File.unlink("test_stderr.#{$$}.log") rescue nil
|
34
|
+
File.unlink("test_stdout.#{$$}.log") rescue nil
|
35
|
+
end
|
36
|
+
|
37
|
+
begin
|
38
|
+
yield
|
39
|
+
ensure
|
40
|
+
STDERR.reopen(orig_err)
|
41
|
+
STDOUT.reopen(orig_out)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Either takes a string to do a get request against, or a tuple of [URI, HTTP] where
|
46
|
+
# HTTP is some kind of Net::HTTP request object (POST, HEAD, etc.)
|
47
|
+
def hit(uris)
|
48
|
+
results = []
|
49
|
+
uris.each do |u|
|
50
|
+
res = nil
|
51
|
+
|
52
|
+
if u.kind_of? String
|
53
|
+
res = Net::HTTP.get(URI.parse(u))
|
54
|
+
else
|
55
|
+
url = URI.parse(u[0])
|
56
|
+
res = Net::HTTP.new(url.host, url.port).start {|h| h.request(u[1]) }
|
57
|
+
end
|
58
|
+
|
59
|
+
assert res != nil, "Didn't get a response: #{u}"
|
60
|
+
results << res
|
61
|
+
end
|
62
|
+
|
63
|
+
return results
|
64
|
+
end
|
65
|
+
|
66
|
+
# unused_port provides an unused port on +addr+ usable for TCP that is
|
67
|
+
# guaranteed to be unused across all unicorn builds on that system. It
|
68
|
+
# prevents race conditions by using a lock file other unicorn builds
|
69
|
+
# will see. This is required if you perform several builds in parallel
|
70
|
+
# with a continuous integration system or run tests in parallel via
|
71
|
+
# gmake. This is NOT guaranteed to be race-free if you run other
|
72
|
+
# processes that bind to random ports for testing (but the window
|
73
|
+
# for a race condition is very small). You may also set UNICORN_TEST_ADDR
|
74
|
+
# to override the default test address (127.0.0.1).
|
75
|
+
def unused_port(addr = '127.0.0.1')
|
76
|
+
retries = 100
|
77
|
+
base = 5000
|
78
|
+
port = sock = nil
|
79
|
+
begin
|
80
|
+
begin
|
81
|
+
port = base + rand(32768 - base)
|
82
|
+
sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
|
83
|
+
sock.bind(Socket.pack_sockaddr_in(port, addr))
|
84
|
+
sock.listen(5)
|
85
|
+
rescue Errno::EADDRINUSE, Errno::EACCES
|
86
|
+
sock.close rescue nil
|
87
|
+
retry if (retries -= 1) >= 0
|
88
|
+
end
|
89
|
+
|
90
|
+
# since we'll end up closing the random port we just got, there's a race
|
91
|
+
# condition could allow the random port we just chose to reselect itself
|
92
|
+
# when running tests in parallel with gmake. Create a lock file while
|
93
|
+
# we have the port here to ensure that does not happen .
|
94
|
+
lock_path = "#{Dir::tmpdir}/unicorn_test.#{addr}:#{port}.lock"
|
95
|
+
lock = File.open(lock_path, File::WRONLY|File::CREAT|File::EXCL, 0600)
|
96
|
+
at_exit { File.unlink(lock_path) rescue nil }
|
97
|
+
rescue Errno::EEXIST
|
98
|
+
sock.close rescue nil
|
99
|
+
retry
|
100
|
+
end
|
101
|
+
sock.close rescue nil
|
102
|
+
port
|
103
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
def do_test(st, chunk)
|
5
|
+
s = TCPSocket.new('127.0.0.1',ARGV[0].to_i);
|
6
|
+
req = StringIO.new(st)
|
7
|
+
nout = 0
|
8
|
+
randstop = rand(st.length / 10)
|
9
|
+
STDERR.puts "stopping after: #{randstop}"
|
10
|
+
|
11
|
+
begin
|
12
|
+
while data = req.read(chunk)
|
13
|
+
nout += s.write(data)
|
14
|
+
s.flush
|
15
|
+
sleep 0.1
|
16
|
+
if nout > randstop
|
17
|
+
STDERR.puts "BANG! after #{nout} bytes."
|
18
|
+
break
|
19
|
+
end
|
20
|
+
end
|
21
|
+
rescue Object => e
|
22
|
+
STDERR.puts "ERROR: #{e}"
|
23
|
+
ensure
|
24
|
+
s.close
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
content = "-" * (1024 * 240)
|
29
|
+
st = "GET / HTTP/1.1\r\nHost: www.zedshaw.com\r\nContent-Type: text/plain\r\nContent-Length: #{content.length}\r\n\r\n#{content}"
|
30
|
+
|
31
|
+
puts "length: #{content.length}"
|
32
|
+
|
33
|
+
threads = []
|
34
|
+
ARGV[1].to_i.times do
|
35
|
+
t = Thread.new do
|
36
|
+
size = 100
|
37
|
+
puts ">>>> #{size} sized chunks"
|
38
|
+
do_test(st, size)
|
39
|
+
end
|
40
|
+
|
41
|
+
t.abort_on_exception = true
|
42
|
+
threads << t
|
43
|
+
end
|
44
|
+
|
45
|
+
threads.each {|t| t.join}
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'unicorn/configurator'
|
4
|
+
|
5
|
+
class TestConfigurator < Test::Unit::TestCase
|
6
|
+
|
7
|
+
def test_config_defaults
|
8
|
+
assert_nothing_raised { Unicorn::Configurator.new {} }
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_config_invalid
|
12
|
+
tmp = Tempfile.new('unicorn_config')
|
13
|
+
tmp.syswrite(%q(asdfasdf "hello-world"))
|
14
|
+
assert_raises(NoMethodError) do
|
15
|
+
Unicorn::Configurator.new(:config_file => tmp.path)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_config_non_existent
|
20
|
+
tmp = Tempfile.new('unicorn_config')
|
21
|
+
path = tmp.path
|
22
|
+
tmp.close!
|
23
|
+
assert_raises(Errno::ENOENT) do
|
24
|
+
Unicorn::Configurator.new(:config_file => path)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_config_defaults
|
29
|
+
cfg = Unicorn::Configurator.new(:use_defaults => true)
|
30
|
+
assert_nothing_raised { cfg.commit!(self) }
|
31
|
+
Unicorn::Configurator::DEFAULTS.each do |key,value|
|
32
|
+
assert_equal value, instance_variable_get("@#{key.to_s}")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_config_defaults_skip
|
37
|
+
cfg = Unicorn::Configurator.new(:use_defaults => true)
|
38
|
+
skip = [ :logger ]
|
39
|
+
assert_nothing_raised { cfg.commit!(self, :skip => skip) }
|
40
|
+
@logger = nil
|
41
|
+
Unicorn::Configurator::DEFAULTS.each do |key,value|
|
42
|
+
next if skip.include?(key)
|
43
|
+
assert_equal value, instance_variable_get("@#{key.to_s}")
|
44
|
+
end
|
45
|
+
assert_nil @logger
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# Copyright (c) 2005 Zed A. Shaw
|
2
|
+
# You can redistribute it and/or modify it under the same terms as Ruby.
|
3
|
+
#
|
4
|
+
# Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html
|
5
|
+
# for more information.
|
6
|
+
|
7
|
+
require 'test/test_helper'
|
8
|
+
|
9
|
+
include Unicorn
|
10
|
+
|
11
|
+
class HttpParserTest < Test::Unit::TestCase
|
12
|
+
|
13
|
+
def test_parse_simple
|
14
|
+
parser = HttpParser.new
|
15
|
+
req = {}
|
16
|
+
http = "GET / HTTP/1.1\r\n\r\n"
|
17
|
+
nread = parser.execute(req, http, 0)
|
18
|
+
|
19
|
+
assert nread == http.length, "Failed to parse the full HTTP request"
|
20
|
+
assert parser.finished?, "Parser didn't finish"
|
21
|
+
assert !parser.error?, "Parser had error"
|
22
|
+
assert nread == parser.nread, "Number read returned from execute does not match"
|
23
|
+
|
24
|
+
assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
|
25
|
+
assert_equal '/', req['REQUEST_PATH']
|
26
|
+
assert_equal 'HTTP/1.1', req['HTTP_VERSION']
|
27
|
+
assert_equal '/', req['REQUEST_URI']
|
28
|
+
assert_equal 'CGI/1.2', req['GATEWAY_INTERFACE']
|
29
|
+
assert_equal 'GET', req['REQUEST_METHOD']
|
30
|
+
assert_nil req['FRAGMENT']
|
31
|
+
assert_nil req['QUERY_STRING']
|
32
|
+
|
33
|
+
parser.reset
|
34
|
+
assert parser.nread == 0, "Number read after reset should be 0"
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_parse_strange_headers
|
38
|
+
parser = HttpParser.new
|
39
|
+
req = {}
|
40
|
+
should_be_good = "GET / HTTP/1.1\r\naaaaaaaaaaaaa:++++++++++\r\n\r\n"
|
41
|
+
nread = parser.execute(req, should_be_good, 0)
|
42
|
+
assert_equal should_be_good.length, nread
|
43
|
+
assert parser.finished?
|
44
|
+
assert !parser.error?
|
45
|
+
|
46
|
+
# ref: http://thread.gmane.org/gmane.comp.lang.ruby.Unicorn.devel/37/focus=45
|
47
|
+
# (note we got 'pen' mixed up with 'pound' in that thread,
|
48
|
+
# but the gist of it is still relevant: these nasty headers are irrelevant
|
49
|
+
#
|
50
|
+
# nasty_pound_header = "GET / HTTP/1.1\r\nX-SSL-Bullshit: -----BEGIN CERTIFICATE-----\r\n\tMIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx\r\n\tETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT\r\n\tAkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu\r\n\tdWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV\r\n\tSzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV\r\n\tBAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB\r\n\tBQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF\r\n\tW51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR\r\n\tgW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL\r\n\t0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP\r\n\tu2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR\r\n\twgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG\r\n\tA1UdEwEB/wQCMAAwEQYJYIZIAYb4QgEBBAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs\r\n\tBglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD\r\n\tVR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj\r\n\tloCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj\r\n\taWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG\r\n\t9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE\r\n\tIjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO\r\n\tBgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1\r\n\tcHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4QgEDBDAWLmh0\r\n\tdHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC5jcmwwPwYD\r\n\tVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv\r\n\tY3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3\r\n\tXCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8\r\n\tUO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk\r\n\thTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK\r\n\twTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu\r\n\tYhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3\r\n\tRA==\r\n\t-----END CERTIFICATE-----\r\n\r\n"
|
51
|
+
# parser = HttpParser.new
|
52
|
+
# req = {}
|
53
|
+
# nread = parser.execute(req, nasty_pound_header, 0)
|
54
|
+
# assert_equal nasty_pound_header.length, nread
|
55
|
+
# assert parser.finished?
|
56
|
+
# assert !parser.error?
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_parse_ie6_urls
|
60
|
+
%w(/some/random/path"
|
61
|
+
/some/random/path>
|
62
|
+
/some/random/path<
|
63
|
+
/we/love/you/ie6?q=<"">
|
64
|
+
/url?<="&>="
|
65
|
+
/mal"formed"?
|
66
|
+
).each do |path|
|
67
|
+
parser = HttpParser.new
|
68
|
+
req = {}
|
69
|
+
sorta_safe = %(GET #{path} HTTP/1.1\r\n\r\n)
|
70
|
+
nread = parser.execute(req, sorta_safe, 0)
|
71
|
+
assert_equal sorta_safe.length, nread
|
72
|
+
assert parser.finished?
|
73
|
+
assert !parser.error?
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_parse_error
|
78
|
+
parser = HttpParser.new
|
79
|
+
req = {}
|
80
|
+
bad_http = "GET / SsUTF/1.1"
|
81
|
+
|
82
|
+
error = false
|
83
|
+
begin
|
84
|
+
nread = parser.execute(req, bad_http, 0)
|
85
|
+
rescue => details
|
86
|
+
error = true
|
87
|
+
end
|
88
|
+
|
89
|
+
assert error, "failed to throw exception"
|
90
|
+
assert !parser.finished?, "Parser shouldn't be finished"
|
91
|
+
assert parser.error?, "Parser SHOULD have error"
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_fragment_in_uri
|
95
|
+
parser = HttpParser.new
|
96
|
+
req = {}
|
97
|
+
get = "GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n"
|
98
|
+
assert_nothing_raised do
|
99
|
+
parser.execute(req, get, 0)
|
100
|
+
end
|
101
|
+
assert parser.finished?
|
102
|
+
assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI']
|
103
|
+
assert_equal 'posts-17408', req['FRAGMENT']
|
104
|
+
end
|
105
|
+
|
106
|
+
# lame random garbage maker
|
107
|
+
def rand_data(min, max, readable=true)
|
108
|
+
count = min + ((rand(max)+1) *10).to_i
|
109
|
+
res = count.to_s + "/"
|
110
|
+
|
111
|
+
if readable
|
112
|
+
res << Digest::SHA1.hexdigest(rand(count * 100).to_s) * (count / 40)
|
113
|
+
else
|
114
|
+
res << Digest::SHA1.digest(rand(count * 100).to_s) * (count / 20)
|
115
|
+
end
|
116
|
+
|
117
|
+
return res
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
def test_horrible_queries
|
122
|
+
parser = HttpParser.new
|
123
|
+
|
124
|
+
# then that large header names are caught
|
125
|
+
10.times do |c|
|
126
|
+
get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-#{rand_data(1024, 1024+(c*1024))}: Test\r\n\r\n"
|
127
|
+
assert_raises Unicorn::HttpParserError do
|
128
|
+
parser.execute({}, get, 0)
|
129
|
+
parser.reset
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# then that large mangled field values are caught
|
134
|
+
10.times do |c|
|
135
|
+
get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-Test: #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n"
|
136
|
+
assert_raises Unicorn::HttpParserError do
|
137
|
+
parser.execute({}, get, 0)
|
138
|
+
parser.reset
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# then large headers are rejected too
|
143
|
+
get = "GET /#{rand_data(10,120)} HTTP/1.1\r\n"
|
144
|
+
get << "X-Test: test\r\n" * (80 * 1024)
|
145
|
+
assert_raises Unicorn::HttpParserError do
|
146
|
+
parser.execute({}, get, 0)
|
147
|
+
parser.reset
|
148
|
+
end
|
149
|
+
|
150
|
+
# finally just that random garbage gets blocked all the time
|
151
|
+
10.times do |c|
|
152
|
+
get = "GET #{rand_data(1024, 1024+(c*1024), false)} #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n"
|
153
|
+
assert_raises Unicorn::HttpParserError do
|
154
|
+
parser.execute({}, get, 0)
|
155
|
+
parser.reset
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# Copyright (c) 2005 Zed A. Shaw
|
2
|
+
# You can redistribute it and/or modify it under the same terms as Ruby.
|
3
|
+
#
|
4
|
+
# Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html
|
5
|
+
# for more information.
|
6
|
+
|
7
|
+
require 'test/test_helper'
|
8
|
+
|
9
|
+
include Unicorn
|
10
|
+
|
11
|
+
class ResponseTest < Test::Unit::TestCase
|
12
|
+
|
13
|
+
def test_response_headers
|
14
|
+
out = StringIO.new
|
15
|
+
HttpResponse.write(out,[200, {"X-Whatever" => "stuff"}, ["cool"]])
|
16
|
+
|
17
|
+
assert out.length > 0, "output didn't have data"
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_response_OFS_set
|
21
|
+
old_ofs = $,
|
22
|
+
$, = "\f\v"
|
23
|
+
out = StringIO.new
|
24
|
+
HttpResponse.write(out,[200, {"X-Whatever" => "stuff"}, ["cool"]])
|
25
|
+
resp = out.read
|
26
|
+
assert ! resp.include?("\f\v"), "output didn't use $, ($OFS)"
|
27
|
+
ensure
|
28
|
+
$, = old_ofs
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_response_200
|
32
|
+
io = StringIO.new
|
33
|
+
HttpResponse.write(io, [200, {}, []])
|
34
|
+
assert io.length > 0, "output didn't have data"
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_response_with_default_reason
|
38
|
+
code = 400
|
39
|
+
io = StringIO.new
|
40
|
+
HttpResponse.write(io, [code, {}, []])
|
41
|
+
io.rewind
|
42
|
+
assert_match(/.* #{HTTP_STATUS_CODES[code]}$/, io.readline.chomp, "wrong default reason phrase")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# Copyright (c) 2005 Zed A. Shaw
|
2
|
+
# You can redistribute it and/or modify it under the same terms as Ruby.
|
3
|
+
#
|
4
|
+
# Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html
|
5
|
+
# for more information.
|
6
|
+
|
7
|
+
require 'test/test_helper'
|
8
|
+
|
9
|
+
include Unicorn
|
10
|
+
|
11
|
+
class TestHandler
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
# response.socket.write("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhello!\n")
|
15
|
+
[200, { 'Content-Type' => 'text/plain' }, ['hello!\n']]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
class WebServerTest < Test::Unit::TestCase
|
21
|
+
|
22
|
+
def setup
|
23
|
+
@valid_request = "GET / HTTP/1.1\r\nHost: www.zedshaw.com\r\nContent-Type: text/plain\r\n\r\n"
|
24
|
+
@port = unused_port
|
25
|
+
@tester = TestHandler.new
|
26
|
+
redirect_test_io do
|
27
|
+
@server = HttpServer.new(@tester, :listeners => [ "127.0.0.1:#{@port}" ] )
|
28
|
+
end
|
29
|
+
@server.start
|
30
|
+
end
|
31
|
+
|
32
|
+
def teardown
|
33
|
+
redirect_test_io do
|
34
|
+
@server.stop(true)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_simple_server
|
39
|
+
results = hit(["http://localhost:#{@port}/test"])
|
40
|
+
assert_equal 'hello!\n', results[0], "Handler didn't really run"
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def do_test(string, chunk, close_after=nil, shutdown_delay=0)
|
45
|
+
# Do not use instance variables here, because it needs to be thread safe
|
46
|
+
socket = TCPSocket.new("127.0.0.1", @port);
|
47
|
+
request = StringIO.new(string)
|
48
|
+
chunks_out = 0
|
49
|
+
|
50
|
+
while data = request.read(chunk)
|
51
|
+
chunks_out += socket.write(data)
|
52
|
+
socket.flush
|
53
|
+
sleep 0.2
|
54
|
+
if close_after and chunks_out > close_after
|
55
|
+
socket.close
|
56
|
+
sleep 1
|
57
|
+
end
|
58
|
+
end
|
59
|
+
sleep(shutdown_delay)
|
60
|
+
socket.write(" ") # Some platforms only raise the exception on attempted write
|
61
|
+
socket.flush
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_trickle_attack
|
65
|
+
do_test(@valid_request, 3)
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_close_client
|
69
|
+
assert_raises IOError do
|
70
|
+
do_test(@valid_request, 10, 20)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_bad_client
|
75
|
+
redirect_test_io do
|
76
|
+
do_test("GET /test HTTP/BAD", 3)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_header_is_too_long
|
81
|
+
redirect_test_io do
|
82
|
+
long = "GET /test HTTP/1.1\r\n" + ("X-Big: stuff\r\n" * 15000) + "\r\n"
|
83
|
+
assert_raises Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EINVAL, IOError do
|
84
|
+
do_test(long, long.length/2, 10)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_file_streamed_request
|
90
|
+
body = "a" * (Unicorn::Const::MAX_BODY * 2)
|
91
|
+
long = "GET /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body
|
92
|
+
do_test(long, Unicorn::Const::CHUNK_SIZE * 2 -400)
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|