thin 1.2.4-x86-mswin32 → 1.2.6-x86-mswin32
Sign up to get free protection for your applications and to get access to all the features.
- 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
|