thin 1.2.4-x86-mswin32 → 1.2.6-x86-mswin32
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/CHANGELOG +10 -0
- data/Rakefile +7 -4
- data/example/thin.god +1 -1
- data/ext/thin_parser/common.rl +1 -1
- data/ext/thin_parser/parser.c +1026 -293
- data/ext/thin_parser/thin.c +3 -0
- data/lib/rack/adapter/loader.rb +12 -0
- data/lib/rack/adapter/rails.rb +2 -3
- data/lib/thin.rb +24 -24
- data/lib/thin/controllers/cluster.rb +57 -6
- data/lib/thin/controllers/controller.rb +1 -2
- data/lib/thin/daemonizing.rb +5 -3
- data/lib/thin/request.rb +7 -3
- data/lib/thin/runner.rb +5 -2
- data/lib/thin/server.rb +2 -1
- data/lib/thin/version.rb +2 -2
- data/lib/thin_parser.so +0 -0
- data/spec/controllers/cluster_spec.rb +32 -0
- data/spec/rack/loader_spec.rb +13 -0
- data/spec/request/parser_spec.rb +29 -1
- data/spec/request/processing_spec.rb +5 -0
- data/spec/server_spec.rb +4 -0
- data/spec/spec_helper.rb +1 -1
- data/tasks/gem.rake +5 -13
- data/tasks/spec.rake +39 -45
- metadata +6 -33
data/ext/thin_parser/thin.c
CHANGED
@@ -207,6 +207,9 @@ static void header_done(void *data, const char *at, size_t length)
|
|
207
207
|
if (rb_hash_aref(req, global_query_string) == Qnil) {
|
208
208
|
rb_hash_aset(req, global_query_string, global_empty);
|
209
209
|
}
|
210
|
+
if (rb_hash_aref(req, global_path_info) == Qnil) {
|
211
|
+
rb_hash_aset(req, global_path_info, global_empty);
|
212
|
+
}
|
210
213
|
|
211
214
|
/* set some constants */
|
212
215
|
rb_hash_aset(req, global_server_protocol, global_server_protocol_value);
|
data/lib/rack/adapter/loader.rb
CHANGED
@@ -8,6 +8,7 @@ module Rack
|
|
8
8
|
# NOTE: If a framework has a file that is not unique, make sure to place
|
9
9
|
# it at the end.
|
10
10
|
ADAPTERS = [
|
11
|
+
[:rack, 'config.ru'],
|
11
12
|
[:rails, 'config/environment.rb'],
|
12
13
|
[:ramaze, 'start.rb'],
|
13
14
|
[:halcyon, 'runner.ru'],
|
@@ -29,9 +30,20 @@ module Rack
|
|
29
30
|
raise AdapterNotFound, "No adapter found for #{dir}"
|
30
31
|
end
|
31
32
|
|
33
|
+
# Load a Rack application from a Rack config file (.ru).
|
34
|
+
def self.load(config)
|
35
|
+
rackup_code = ::File.read(config)
|
36
|
+
eval("Rack::Builder.new {( #{rackup_code}\n )}.to_app", TOPLEVEL_BINDING, config)
|
37
|
+
end
|
38
|
+
|
32
39
|
# Loads an adapter identified by +name+ using +options+ hash.
|
33
40
|
def self.for(name, options={})
|
41
|
+
ENV['RACK_ENV'] = options[:environment]
|
42
|
+
|
34
43
|
case name.to_sym
|
44
|
+
when :rack
|
45
|
+
return load(::File.join(options[:chdir], "config.ru"))
|
46
|
+
|
35
47
|
when :rails
|
36
48
|
return Rails.new(options.merge(:root => options[:chdir]))
|
37
49
|
|
data/lib/rack/adapter/rails.rb
CHANGED
@@ -32,9 +32,8 @@ module Rack
|
|
32
32
|
end
|
33
33
|
|
34
34
|
def rack_based?
|
35
|
-
|
36
|
-
|
37
|
-
ActionController::Dispatcher.instance_methods.include?("call"))
|
35
|
+
rails_version = ::Rails::VERSION
|
36
|
+
rails_version::MAJOR >= 2 && rails_version::MINOR >= 2 && rails_version::TINY >= 3
|
38
37
|
end
|
39
38
|
|
40
39
|
def load_application
|
data/lib/thin.rb
CHANGED
@@ -3,44 +3,44 @@ require 'timeout'
|
|
3
3
|
require 'stringio'
|
4
4
|
require 'time'
|
5
5
|
require 'forwardable'
|
6
|
-
|
7
6
|
require 'openssl'
|
8
7
|
require 'eventmachine'
|
9
|
-
|
10
|
-
require 'thin/version'
|
11
|
-
require 'thin/statuses'
|
8
|
+
require 'rack'
|
12
9
|
|
13
10
|
module Thin
|
14
|
-
|
15
|
-
|
16
|
-
autoload :
|
17
|
-
autoload :
|
18
|
-
autoload :
|
19
|
-
autoload :
|
20
|
-
autoload :
|
21
|
-
autoload :
|
22
|
-
autoload :
|
23
|
-
autoload :
|
11
|
+
ROOT = File.expand_path(File.dirname(__FILE__))
|
12
|
+
|
13
|
+
autoload :Command, "#{ROOT}/thin/command"
|
14
|
+
autoload :Connection, "#{ROOT}/thin/connection"
|
15
|
+
autoload :Daemonizable, "#{ROOT}/thin/daemonizing"
|
16
|
+
autoload :Logging, "#{ROOT}/thin/logging"
|
17
|
+
autoload :Headers, "#{ROOT}/thin/headers"
|
18
|
+
autoload :Request, "#{ROOT}/thin/request"
|
19
|
+
autoload :Response, "#{ROOT}/thin/response"
|
20
|
+
autoload :Runner, "#{ROOT}/thin/runner"
|
21
|
+
autoload :Server, "#{ROOT}/thin/server"
|
22
|
+
autoload :Stats, "#{ROOT}/thin/stats"
|
24
23
|
|
25
24
|
module Backends
|
26
|
-
autoload :Base,
|
27
|
-
autoload :SwiftiplyClient,
|
28
|
-
autoload :TcpServer,
|
29
|
-
autoload :UnixServer,
|
25
|
+
autoload :Base, "#{ROOT}/thin/backends/base"
|
26
|
+
autoload :SwiftiplyClient, "#{ROOT}/thin/backends/swiftiply_client"
|
27
|
+
autoload :TcpServer, "#{ROOT}/thin/backends/tcp_server"
|
28
|
+
autoload :UnixServer, "#{ROOT}/thin/backends/unix_server"
|
30
29
|
end
|
31
30
|
|
32
31
|
module Controllers
|
33
|
-
autoload :Cluster,
|
34
|
-
autoload :Controller,
|
35
|
-
autoload :Service,
|
32
|
+
autoload :Cluster, "#{ROOT}/thin/controllers/cluster"
|
33
|
+
autoload :Controller, "#{ROOT}/thin/controllers/controller"
|
34
|
+
autoload :Service, "#{ROOT}/thin/controllers/service"
|
36
35
|
end
|
37
36
|
end
|
38
37
|
|
39
|
-
require
|
40
|
-
require
|
38
|
+
require "#{Thin::ROOT}/thin/version"
|
39
|
+
require "#{Thin::ROOT}/thin/statuses"
|
40
|
+
require "#{Thin::ROOT}/rack/adapter/loader"
|
41
41
|
|
42
42
|
module Rack
|
43
43
|
module Adapter
|
44
|
-
autoload :Rails,
|
44
|
+
autoload :Rails, "#{Thin::ROOT}/rack/adapter/rails"
|
45
45
|
end
|
46
46
|
end
|
@@ -1,4 +1,9 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
1
3
|
module Thin
|
4
|
+
# An exception class to handle the event that server didn't start on time
|
5
|
+
class RestartTimeout < RuntimeError; end
|
6
|
+
|
2
7
|
module Controllers
|
3
8
|
# Control a set of servers.
|
4
9
|
# * Generate start and stop commands and run them.
|
@@ -7,7 +12,10 @@ module Thin
|
|
7
12
|
class Cluster < Controller
|
8
13
|
# Cluster only options that should not be passed in the command sent
|
9
14
|
# to the indiviual servers.
|
10
|
-
CLUSTER_OPTIONS = [:servers, :only]
|
15
|
+
CLUSTER_OPTIONS = [:servers, :only, :onebyone, :wait]
|
16
|
+
|
17
|
+
# Maximum wait time for the server to be restarted
|
18
|
+
DEFAULT_WAIT_TIME = 30 # seconds
|
11
19
|
|
12
20
|
# Create a new cluster of servers launched using +options+.
|
13
21
|
def initialize(options)
|
@@ -15,7 +23,7 @@ module Thin
|
|
15
23
|
# Cluster can only contain daemonized servers
|
16
24
|
@options.merge!(:daemonize => true)
|
17
25
|
end
|
18
|
-
|
26
|
+
|
19
27
|
def first_port; @options[:port] end
|
20
28
|
def address; @options[:address] end
|
21
29
|
def socket; @options[:socket] end
|
@@ -23,7 +31,9 @@ module Thin
|
|
23
31
|
def log_file; @options[:log] end
|
24
32
|
def size; @options[:servers] end
|
25
33
|
def only; @options[:only] end
|
26
|
-
|
34
|
+
def onebyone; @options[:onebyone] end
|
35
|
+
def wait; @options[:wait] end
|
36
|
+
|
27
37
|
def swiftiply?
|
28
38
|
@options.has_key?(:swiftiply)
|
29
39
|
end
|
@@ -54,9 +64,50 @@ module Thin
|
|
54
64
|
|
55
65
|
# Stop and start the servers.
|
56
66
|
def restart
|
57
|
-
|
58
|
-
|
59
|
-
|
67
|
+
unless onebyone
|
68
|
+
# Let's do a normal restart by defaults
|
69
|
+
stop
|
70
|
+
sleep 0.1 # Let's breath a bit shall we ?
|
71
|
+
start
|
72
|
+
else
|
73
|
+
with_each_server do |n|
|
74
|
+
stop_server(n)
|
75
|
+
sleep 0.1 # Let's breath a bit shall we ?
|
76
|
+
start_server(n)
|
77
|
+
wait_until_server_started(n)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_socket(number)
|
83
|
+
if socket
|
84
|
+
UNIXSocket.new(socket_for(number))
|
85
|
+
else
|
86
|
+
TCPSocket.new(address, number)
|
87
|
+
end
|
88
|
+
rescue
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
# Make sure the server is running before moving on to the next one.
|
93
|
+
def wait_until_server_started(number)
|
94
|
+
log "Waiting for server to start ..."
|
95
|
+
STDOUT.flush # Need this to make sure user got the message
|
96
|
+
|
97
|
+
tries = 0
|
98
|
+
loop do
|
99
|
+
if test_socket = test_socket(number)
|
100
|
+
test_socket.close
|
101
|
+
break
|
102
|
+
elsif tries < wait
|
103
|
+
sleep 1
|
104
|
+
tries += 1
|
105
|
+
else
|
106
|
+
raise RestartTimeout, "The server didn't start in time. Please look at server's log file " +
|
107
|
+
"for more information, or set the value of 'wait' in your config " +
|
108
|
+
"file to be higher (defaults: 30)."
|
109
|
+
end
|
110
|
+
end
|
60
111
|
end
|
61
112
|
|
62
113
|
def server_id(number)
|
@@ -172,8 +172,7 @@ module Thin
|
|
172
172
|
Kernel.load(@options[:rackup])
|
173
173
|
Object.const_get(File.basename(@options[:rackup], '.rb').capitalize.to_sym)
|
174
174
|
when /\.ru$/
|
175
|
-
|
176
|
-
eval("Rack::Builder.new {( #{rackup_code}\n )}.to_app", TOPLEVEL_BINDING, @options[:rackup])
|
175
|
+
Rack::Adapter.load(@options[:rackup])
|
177
176
|
else
|
178
177
|
raise "Invalid rackup file. please specify either a .ru or .rb file"
|
179
178
|
end
|
data/lib/thin/daemonizing.rb
CHANGED
@@ -36,18 +36,21 @@ module Thin
|
|
36
36
|
def daemonize
|
37
37
|
raise PlatformNotSupported, 'Daemonizing is not supported on Windows' if Thin.win?
|
38
38
|
raise ArgumentError, 'You must specify a pid_file to daemonize' unless @pid_file
|
39
|
-
|
39
|
+
|
40
40
|
remove_stale_pid_file
|
41
41
|
|
42
42
|
pwd = Dir.pwd # Current directory is changed during daemonization, so store it
|
43
43
|
|
44
|
+
# HACK we need to create the directory before daemonization to prevent a bug under 1.9
|
45
|
+
# ignoring all signals when the directory is created after daemonization.
|
46
|
+
FileUtils.mkdir_p File.dirname(@pid_file)
|
47
|
+
|
44
48
|
Daemonize.daemonize(File.expand_path(@log_file), name)
|
45
49
|
|
46
50
|
Dir.chdir(pwd)
|
47
51
|
|
48
52
|
write_pid_file
|
49
53
|
|
50
|
-
trap('HUP') { restart }
|
51
54
|
at_exit do
|
52
55
|
log ">> Exiting!"
|
53
56
|
remove_pid_file
|
@@ -153,7 +156,6 @@ module Thin
|
|
153
156
|
|
154
157
|
def write_pid_file
|
155
158
|
log ">> Writing PID to #{@pid_file}"
|
156
|
-
FileUtils.mkdir_p File.dirname(@pid_file)
|
157
159
|
open(@pid_file,"w") { |f| f.write(Process.pid) }
|
158
160
|
File.chmod(0644, @pid_file)
|
159
161
|
end
|
data/lib/thin/request.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require "#{Thin::ROOT}/thin_parser"
|
2
2
|
require 'tempfile'
|
3
3
|
|
4
4
|
module Thin
|
@@ -13,7 +13,11 @@ module Thin
|
|
13
13
|
MAX_BODY = 1024 * (80 + 32)
|
14
14
|
BODY_TMPFILE = 'thin-body'.freeze
|
15
15
|
MAX_HEADER = 1024 * (80 + 32)
|
16
|
-
|
16
|
+
|
17
|
+
INITIAL_BODY = ''
|
18
|
+
# Force external_encoding of request's body to ASCII_8BIT
|
19
|
+
INITIAL_BODY.encode!(Encoding::ASCII_8BIT) if INITIAL_BODY.respond_to?(:encode!)
|
20
|
+
|
17
21
|
# Freeze some HTTP header names & values
|
18
22
|
SERVER_SOFTWARE = 'SERVER_SOFTWARE'.freeze
|
19
23
|
SERVER_NAME = 'SERVER_NAME'.freeze
|
@@ -49,7 +53,7 @@ module Thin
|
|
49
53
|
@parser = Thin::HttpParser.new
|
50
54
|
@data = ''
|
51
55
|
@nparsed = 0
|
52
|
-
@body = StringIO.new
|
56
|
+
@body = StringIO.new(INITIAL_BODY.dup)
|
53
57
|
@env = {
|
54
58
|
SERVER_SOFTWARE => SERVER,
|
55
59
|
SERVER_NAME => LOCALHOST,
|
data/lib/thin/runner.rb
CHANGED
@@ -41,7 +41,8 @@ module Thin
|
|
41
41
|
:pid => 'tmp/pids/thin.pid',
|
42
42
|
:max_conns => Server::DEFAULT_MAXIMUM_CONNECTIONS,
|
43
43
|
:max_persistent_conns => Server::DEFAULT_MAXIMUM_PERSISTENT_CONNECTIONS,
|
44
|
-
:require => []
|
44
|
+
:require => [],
|
45
|
+
:wait => Controllers::Cluster::DEFAULT_WAIT_TIME
|
45
46
|
}
|
46
47
|
|
47
48
|
parse!
|
@@ -96,6 +97,8 @@ module Thin
|
|
96
97
|
opts.on("-o", "--only NUM", "Send command to only one server of the cluster") { |only| @options[:only] = only.to_i }
|
97
98
|
opts.on("-C", "--config FILE", "Load options from config file") { |file| @options[:config] = file }
|
98
99
|
opts.on( "--all [DIR]", "Send command to each config files in DIR") { |dir| @options[:all] = dir } if Thin.linux?
|
100
|
+
opts.on("-O", "--onebyone", "Restart the cluster one by one (only works with restart command)") { @options[:onebyone] = true }
|
101
|
+
opts.on("-w", "--wait NUM", "Maximum wait time for server to be started in seconds (use with -O)") { |time| @options[:wait] = time.to_i }
|
99
102
|
end
|
100
103
|
|
101
104
|
opts.separator ""
|
@@ -105,7 +108,7 @@ module Thin
|
|
105
108
|
opts.on("-t", "--timeout SEC", "Request or command timeout in sec " +
|
106
109
|
"(default: #{@options[:timeout]})") { |sec| @options[:timeout] = sec.to_i }
|
107
110
|
opts.on("-f", "--force", "Force the execution of the command") { @options[:force] = true }
|
108
|
-
opts.on( "--max-conns NUM", "Maximum number of
|
111
|
+
opts.on( "--max-conns NUM", "Maximum number of open file descriptors " +
|
109
112
|
"(default: #{@options[:max_conns]})",
|
110
113
|
"Might require sudo to set higher then 1024") { |num| @options[:max_conns] = num.to_i } unless Thin.win?
|
111
114
|
opts.on( "--max-persistent-conns NUM",
|
data/lib/thin/server.rb
CHANGED
@@ -208,11 +208,12 @@ module Thin
|
|
208
208
|
protected
|
209
209
|
# Register signals:
|
210
210
|
# * INT calls +stop+ to shutdown gracefully.
|
211
|
-
# * TERM calls <tt>stop!</tt> to force shutdown.
|
211
|
+
# * TERM calls <tt>stop!</tt> to force shutdown.
|
212
212
|
def setup_signals
|
213
213
|
trap('QUIT') { stop } unless Thin.win?
|
214
214
|
trap('INT') { stop! }
|
215
215
|
trap('TERM') { stop! }
|
216
|
+
trap('HUP') { restart }
|
216
217
|
end
|
217
218
|
|
218
219
|
def select_backend(host, port, options)
|
data/lib/thin/version.rb
CHANGED
@@ -6,11 +6,11 @@ module Thin
|
|
6
6
|
module VERSION #:nodoc:
|
7
7
|
MAJOR = 1
|
8
8
|
MINOR = 2
|
9
|
-
TINY =
|
9
|
+
TINY = 6
|
10
10
|
|
11
11
|
STRING = [MAJOR, MINOR, TINY].join('.')
|
12
12
|
|
13
|
-
CODENAME = "
|
13
|
+
CODENAME = "Crazy Delicious".freeze
|
14
14
|
|
15
15
|
RACK = [1, 0].freeze # Rack protocol version
|
16
16
|
end
|
data/lib/thin_parser.so
CHANGED
Binary file
|
@@ -232,4 +232,36 @@ describe Cluster, "with Swiftiply" do
|
|
232
232
|
def options_for_swiftiply(number)
|
233
233
|
{ :address => '0.0.0.0', :port => 3000, :daemonize => true, :log => "thin.#{number}.log", :timeout => 10, :pid => "thin.#{number}.pid", :chdir => "/rails_app", :swiftiply => true }
|
234
234
|
end
|
235
|
+
end
|
236
|
+
|
237
|
+
describe Cluster, "rolling restart" do
|
238
|
+
before do
|
239
|
+
@cluster = Cluster.new(:chdir => '/rails_app',
|
240
|
+
:address => '0.0.0.0',
|
241
|
+
:port => 3000,
|
242
|
+
:servers => 2,
|
243
|
+
:timeout => 10,
|
244
|
+
:log => 'thin.log',
|
245
|
+
:pid => 'thin.pid',
|
246
|
+
:onebyone => true,
|
247
|
+
:wait => 30
|
248
|
+
)
|
249
|
+
end
|
250
|
+
|
251
|
+
it "should restart servers one by one" do
|
252
|
+
Command.should_receive(:run).with(:stop, options_for_port(3000))
|
253
|
+
Command.should_receive(:run).with(:start, options_for_port(3000))
|
254
|
+
@cluster.should_receive(:wait_until_server_started).with(3000)
|
255
|
+
|
256
|
+
Command.should_receive(:run).with(:stop, options_for_port(3001))
|
257
|
+
Command.should_receive(:run).with(:start, options_for_port(3001))
|
258
|
+
@cluster.should_receive(:wait_until_server_started).with(3001)
|
259
|
+
|
260
|
+
@cluster.restart
|
261
|
+
end
|
262
|
+
|
263
|
+
private
|
264
|
+
def options_for_port(port)
|
265
|
+
{ :daemonize => true, :log => "thin.#{port}.log", :timeout => 10, :address => "0.0.0.0", :port => port, :pid => "thin.#{port}.pid", :chdir => "/rails_app" }
|
266
|
+
end
|
235
267
|
end
|
data/spec/rack/loader_spec.rb
CHANGED
@@ -2,9 +2,18 @@ require File.dirname(__FILE__) + '/../spec_helper'
|
|
2
2
|
|
3
3
|
describe Rack::Adapter do
|
4
4
|
before do
|
5
|
+
@config_ru_path = File.dirname(__FILE__) + '/../../example'
|
5
6
|
@rails_path = File.dirname(__FILE__) + '/../rails_app'
|
6
7
|
end
|
7
8
|
|
9
|
+
it "should load Rack app from config" do
|
10
|
+
Rack::Adapter.load(@config_ru_path + '/config.ru').class.should == Proc
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should guess Rack app from dir" do
|
14
|
+
Rack::Adapter.guess(@config_ru_path).should == :rack
|
15
|
+
end
|
16
|
+
|
8
17
|
it "should guess rails app from dir" do
|
9
18
|
Rack::Adapter.guess(@rails_path).should == :rails
|
10
19
|
end
|
@@ -13,6 +22,10 @@ describe Rack::Adapter do
|
|
13
22
|
proc { Rack::Adapter.guess('.') }.should raise_error(Rack::AdapterNotFound)
|
14
23
|
end
|
15
24
|
|
25
|
+
it "should load Rack adapter" do
|
26
|
+
Rack::Adapter.for(:rack, :chdir => @config_ru_path).class.should == Proc
|
27
|
+
end
|
28
|
+
|
16
29
|
it "should load Rails adapter" do
|
17
30
|
Rack::Adapter::Rails.should_receive(:new)
|
18
31
|
Rack::Adapter.for(:rails, :chdir => @rails_path)
|
data/spec/request/parser_spec.rb
CHANGED
@@ -209,7 +209,35 @@ EOS
|
|
209
209
|
parser = HttpParser.new
|
210
210
|
req = {}
|
211
211
|
nread = parser.execute(req, req_str, 0)
|
212
|
-
req.should
|
212
|
+
req.should have_key('HTTP_HOS_T')
|
213
213
|
}
|
214
214
|
end
|
215
|
+
|
216
|
+
it "should parse PATH_INFO with semicolon" do
|
217
|
+
qs = "QUERY_STRING"
|
218
|
+
pi = "PATH_INFO"
|
219
|
+
{
|
220
|
+
"/1;a=b?c=d&e=f" => { qs => "c=d&e=f", pi => "/1;a=b" },
|
221
|
+
"/1?c=d&e=f" => { qs => "c=d&e=f", pi => "/1" },
|
222
|
+
"/1;a=b" => { qs => "", pi => "/1;a=b" },
|
223
|
+
"/1;a=b?" => { qs => "", pi => "/1;a=b" },
|
224
|
+
"/1?a=b;c=d&e=f" => { qs => "a=b;c=d&e=f", pi => "/1" },
|
225
|
+
"*" => { qs => "", pi => "" },
|
226
|
+
}.each do |uri, expect|
|
227
|
+
parser = HttpParser.new
|
228
|
+
env = {}
|
229
|
+
nread = parser.execute(env, "GET #{uri} HTTP/1.1\r\nHost: www.example.com\r\n\r\n", 0)
|
230
|
+
|
231
|
+
env[pi].should == expect[pi]
|
232
|
+
env[qs].should == expect[qs]
|
233
|
+
env["REQUEST_URI"].should == uri
|
234
|
+
|
235
|
+
next if uri == "*"
|
236
|
+
|
237
|
+
# Validate w/ Ruby's URI.parse
|
238
|
+
uri = URI.parse("http://example.com#{uri}")
|
239
|
+
env[qs].should == uri.query.to_s
|
240
|
+
env[pi].should == uri.path
|
241
|
+
end
|
242
|
+
end
|
215
243
|
end
|