serverside 0.2.9 → 0.3.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/CHANGELOG +56 -0
- data/Rakefile +12 -52
- data/bin/serverside +1 -1
- data/lib/serverside/application.rb +2 -1
- data/lib/serverside/caching.rb +62 -50
- data/lib/serverside/controllers.rb +91 -0
- data/lib/serverside/core_ext.rb +6 -0
- data/lib/serverside/daemon.rb +25 -5
- data/lib/serverside/request.rb +17 -11
- data/lib/serverside/routing.rb +11 -10
- data/lib/serverside/server.rb +14 -6
- data/lib/serverside/static.rb +7 -18
- data/lib/serverside/template.rb +20 -12
- data/spec/caching_spec.rb +318 -0
- data/spec/cluster_spec.rb +140 -0
- data/{test/spec → spec}/connection_spec.rb +4 -4
- data/{test/spec/controller_spec.rb → spec/controllers_spec.rb} +15 -12
- data/{test/spec → spec}/core_ext_spec.rb +23 -18
- data/spec/daemon_spec.rb +99 -0
- data/{test/spec → spec}/request_spec.rb +45 -45
- data/spec/routing_spec.rb +240 -0
- data/spec/server_spec.rb +40 -0
- data/spec/static_spec.rb +279 -0
- data/spec/template_spec.rb +129 -0
- metadata +21 -35
- data/lib/serverside/controller.rb +0 -67
- data/test/functional/primitive_static_server_test.rb +0 -61
- data/test/functional/request_body_test.rb +0 -93
- data/test/functional/routing_server.rb +0 -14
- data/test/functional/routing_server_test.rb +0 -41
- data/test/functional/static_profile.rb +0 -17
- data/test/functional/static_rfuzz.rb +0 -31
- data/test/functional/static_server.rb +0 -7
- data/test/functional/static_server_test.rb +0 -31
- data/test/spec/caching_spec.rb +0 -139
- data/test/test_helper.rb +0 -2
- data/test/unit/cluster_test.rb +0 -129
- data/test/unit/connection_test.rb +0 -48
- data/test/unit/core_ext_test.rb +0 -46
- data/test/unit/daemon_test.rb +0 -75
- data/test/unit/request_test.rb +0 -278
- data/test/unit/routing_test.rb +0 -171
- data/test/unit/server_test.rb +0 -28
- data/test/unit/static_test.rb +0 -171
- data/test/unit/template_test.rb +0 -78
data/lib/serverside/static.rb
CHANGED
@@ -6,10 +6,11 @@ module ServerSide
|
|
6
6
|
module Static
|
7
7
|
include HTTP::Caching
|
8
8
|
|
9
|
-
ETAG_FORMAT = '%x:%x:%x'.
|
9
|
+
ETAG_FORMAT = '%x:%x:%x'.freeze
|
10
10
|
TEXT_PLAIN = 'text/plain'.freeze
|
11
11
|
TEXT_HTML = 'text/html'.freeze
|
12
12
|
MAX_CACHE_FILE_SIZE = 100000.freeze # 100KB for the moment
|
13
|
+
MAX_AGE = 86400 # one day
|
13
14
|
|
14
15
|
DIR_LISTING_START = '<html><head><title>Directory Listing for %s</title></head><body><h2>Directory listing for %s:</h2>'.freeze
|
15
16
|
DIR_LISTING = '<a href="%s">%s</a><br/>'.freeze
|
@@ -29,8 +30,6 @@ module ServerSide
|
|
29
30
|
'.png'.freeze => 'image/png'.freeze
|
30
31
|
})
|
31
32
|
|
32
|
-
@@static_files = {}
|
33
|
-
|
34
33
|
# Serves a file over HTTP. The file is cached in memory for later retrieval.
|
35
34
|
# If the If-None-Match header is included with an ETag, it is checked
|
36
35
|
# against the file's current ETag. If there's a match, a 304 response is
|
@@ -38,17 +37,11 @@ module ServerSide
|
|
38
37
|
def serve_file(fn)
|
39
38
|
stat = File.stat(fn)
|
40
39
|
etag = (ETAG_FORMAT % [stat.mtime.to_i, stat.size, stat.ino]).freeze
|
41
|
-
validate_cache(
|
42
|
-
|
43
|
-
|
44
|
-
else
|
45
|
-
content = IO.read(fn).freeze
|
46
|
-
@@static_files[fn] = [etag.freeze, content]
|
47
|
-
end
|
48
|
-
send_response(200, @@mime_types[File.extname(fn)], content, stat.size)
|
40
|
+
validate_cache(stat.mtime, MAX_AGE, etag) do
|
41
|
+
send_response(200, @@mime_types[File.extname(fn)], IO.read(fn),
|
42
|
+
stat.size)
|
49
43
|
end
|
50
44
|
rescue => e
|
51
|
-
puts e.message
|
52
45
|
send_response(404, TEXT_PLAIN, 'Error reading file.')
|
53
46
|
end
|
54
47
|
|
@@ -64,17 +57,13 @@ module ServerSide
|
|
64
57
|
end
|
65
58
|
|
66
59
|
def serve_template(fn, b = nil)
|
67
|
-
|
68
|
-
send_response(200, TEXT_HTML, Template.render(fn, b || binding))
|
69
|
-
end
|
60
|
+
send_response(200, TEXT_HTML, Template.render(fn, b || binding))
|
70
61
|
end
|
71
62
|
|
72
63
|
# Serves static files and directory listings.
|
73
64
|
def serve_static(path)
|
74
65
|
if File.file?(path)
|
75
|
-
serve_file(path)
|
76
|
-
elsif serve_template(path)
|
77
|
-
return
|
66
|
+
path =~ RHTML ? serve_template(path) : serve_file(path)
|
78
67
|
elsif File.directory?(path)
|
79
68
|
if File.file?(path/'index.html')
|
80
69
|
serve_file(path/'index.html')
|
data/lib/serverside/template.rb
CHANGED
@@ -4,33 +4,41 @@ module ServerSide
|
|
4
4
|
# The Template module implements an ERB template rendering system. Templates
|
5
5
|
# are cached and automatically reloaded if the file changes.
|
6
6
|
class Template
|
7
|
-
# The @@templates variable
|
7
|
+
# The @@templates variable caches templates in use. The values are
|
8
8
|
# arrays containing 2 objects: a file stamp (if the template comes from a
|
9
|
-
# file,) and the template object itself.
|
9
|
+
# file,) and the template object itself.
|
10
10
|
@@templates = {}
|
11
11
|
|
12
|
-
#
|
12
|
+
# Caches a template for later use. The stamp parameter is used only when
|
13
13
|
# the content of a template file is stored.
|
14
14
|
def self.set(name, body, stamp = nil)
|
15
15
|
@@templates[name] = [stamp, ERB.new(body)]
|
16
16
|
end
|
17
17
|
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
def self.
|
18
|
+
# Validates the referenced template by checking its stamp. If the name
|
19
|
+
# refers to a file, its stamp is checked against the cache stamp, and it
|
20
|
+
# is reloaded if necessary. The function returns an ERB instance or nil if
|
21
|
+
# the template is not found.
|
22
|
+
def self.validate(name)
|
23
23
|
t = @@templates[name]
|
24
|
-
return t[1]
|
25
|
-
|
24
|
+
return t[1] if t && t[0].nil?
|
26
25
|
if File.file?(name)
|
27
26
|
stamp = File.mtime(name)
|
28
27
|
t = set(name, IO.read(name), stamp) if (!t || (stamp != t[0]))
|
29
|
-
t[1]
|
28
|
+
t[1]
|
30
29
|
else
|
31
30
|
@@templates[name] = nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Renders a template by first validating it, and by invoking it with the
|
35
|
+
# supplied binding.
|
36
|
+
def self.render(name, binding)
|
37
|
+
if template = validate(name)
|
38
|
+
template.result(binding)
|
39
|
+
else
|
32
40
|
raise RuntimeError, 'Template not found.'
|
33
41
|
end
|
34
42
|
end
|
35
|
-
end
|
43
|
+
end
|
36
44
|
end
|
@@ -0,0 +1,318 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../lib/serverside')
|
2
|
+
require 'stringio'
|
3
|
+
include ServerSide::HTTP
|
4
|
+
|
5
|
+
class DummyRequest < Request
|
6
|
+
attr_accessor :socket, :persistent
|
7
|
+
include Caching
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
super(StringIO.new)
|
11
|
+
@headers = {}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context "Caching#disable_caching" do
|
16
|
+
specify "should set the Cache-Control header to no-cache" do
|
17
|
+
r = DummyRequest.new
|
18
|
+
r.response_headers['Cache-Control'].should_be_nil
|
19
|
+
r.disable_caching
|
20
|
+
r.response_headers['Cache-Control'].should == 'no-cache'
|
21
|
+
end
|
22
|
+
|
23
|
+
specify "should remove all other cache-related headers" do
|
24
|
+
r = DummyRequest.new
|
25
|
+
r.response_headers['ETag'] = 'something'
|
26
|
+
r.response_headers['Vary'] = 'something'
|
27
|
+
r.response_headers['Expires'] = 'something'
|
28
|
+
r.response_headers['Last-Modified'] = 'something'
|
29
|
+
r.disable_caching
|
30
|
+
r.response_headers['ETag'].should_be_nil
|
31
|
+
r.response_headers['Vary'].should_be_nil
|
32
|
+
r.response_headers['Expires'].should_be_nil
|
33
|
+
r.response_headers['Last-Modified'].should_be_nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "Caching#etag_validators" do
|
38
|
+
specify "should return an empty array if no validators are present" do
|
39
|
+
r = DummyRequest.new
|
40
|
+
r.etag_validators.should == []
|
41
|
+
end
|
42
|
+
|
43
|
+
specify "should return an array containing all etag validators" do
|
44
|
+
r = DummyRequest.new
|
45
|
+
r.headers['If-None-Match'] = '"aaa-bbb"'
|
46
|
+
r.etag_validators.should == ['aaa-bbb']
|
47
|
+
|
48
|
+
r.headers['If-None-Match'] = '"aaa-bbb", "ccc-ddd"'
|
49
|
+
r.etag_validators.should == ['aaa-bbb', 'ccc-ddd']
|
50
|
+
end
|
51
|
+
|
52
|
+
specify "should handle etags with and without quotes" do
|
53
|
+
r = DummyRequest.new
|
54
|
+
r.headers['If-None-Match'] = 'aaa-bbb'
|
55
|
+
r.etag_validators.should == ['aaa-bbb']
|
56
|
+
|
57
|
+
r.headers['If-None-Match'] = 'aaa-bbb, "ccc-ddd"'
|
58
|
+
r.etag_validators.should == ['aaa-bbb', 'ccc-ddd']
|
59
|
+
end
|
60
|
+
|
61
|
+
specify "should handle a wildcard validator" do
|
62
|
+
r = DummyRequest.new
|
63
|
+
r.headers['If-None-Match'] = '*'
|
64
|
+
r.etag_validators.should == ['*']
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "Caching#valid_etag?" do
|
69
|
+
specify "should return nil if no validator matches the specified etag" do
|
70
|
+
r = DummyRequest.new
|
71
|
+
r.valid_etag?('xxx-yyy').should_be_nil
|
72
|
+
|
73
|
+
r.headers['If-None-Match'] = 'xx-yy, aaa-bbb'
|
74
|
+
r.valid_etag?('xxx-yyy').should_be_nil
|
75
|
+
end
|
76
|
+
|
77
|
+
specify "should return true if a validator matches the specifed etag" do
|
78
|
+
r = DummyRequest.new
|
79
|
+
|
80
|
+
r.headers['If-None-Match'] = 'xxx-yyy'
|
81
|
+
r.valid_etag?('xxx-yyy').should_be true
|
82
|
+
|
83
|
+
r.headers['If-None-Match'] = '"xxx-yyy"'
|
84
|
+
r.valid_etag?('xxx-yyy').should_be true
|
85
|
+
|
86
|
+
r.headers['If-None-Match'] = 'aaa-bbb, xxx-yyy'
|
87
|
+
r.valid_etag?('xxx-yyy').should_be true
|
88
|
+
|
89
|
+
r.headers['If-None-Match'] = 'xxx-yyy, aaa-bbb'
|
90
|
+
r.valid_etag?('xxx-yyy').should_be true
|
91
|
+
end
|
92
|
+
|
93
|
+
specify "should return true if a wildcard is included in If-None-Match" do
|
94
|
+
r = DummyRequest.new
|
95
|
+
|
96
|
+
r.headers['If-None-Match'] = '*'
|
97
|
+
r.valid_etag?('xxx-yyy').should_be true
|
98
|
+
|
99
|
+
r.headers['If-None-Match'] = 'aaa-bbb, *'
|
100
|
+
r.valid_etag?('xxx-yyy').should_be true
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context "Caching#valid_etag? in expiry etag mode (no etag specified)" do
|
105
|
+
specify "should return nil if no etag validator is included" do
|
106
|
+
r = DummyRequest.new
|
107
|
+
r.valid_etag?.should_be_nil
|
108
|
+
end
|
109
|
+
|
110
|
+
specify "should return true if If-None-Match includes a wildcard" do
|
111
|
+
r = DummyRequest.new
|
112
|
+
|
113
|
+
r.headers['If-None-Match'] = '*'
|
114
|
+
r.valid_etag?.should_be true
|
115
|
+
end
|
116
|
+
|
117
|
+
specify "should ignore validators not formatted as expiry etags" do
|
118
|
+
r = DummyRequest.new
|
119
|
+
|
120
|
+
r.headers['If-None-Match'] = 'abcd'
|
121
|
+
r.valid_etag?.should_be_nil
|
122
|
+
|
123
|
+
r.headers['If-None-Match'] = 'xxx-yyy, zzz-zzz'
|
124
|
+
r.valid_etag?.should_be_nil
|
125
|
+
end
|
126
|
+
|
127
|
+
specify "should parse expiry etags and check the expiration stamp" do
|
128
|
+
r = DummyRequest.new
|
129
|
+
t = Time.now
|
130
|
+
fmt = Caching::EXPIRY_ETAG_FORMAT
|
131
|
+
|
132
|
+
r.headers['If-None-Match'] = fmt % [t.to_i, (t - 20).to_i]
|
133
|
+
r.valid_etag?.should_be_nil
|
134
|
+
|
135
|
+
r.headers['If-None-Match'] = fmt % [t.to_i, (t + 20).to_i]
|
136
|
+
r.valid_etag?.should_be true
|
137
|
+
|
138
|
+
r.headers['If-None-Match'] = "xxx-yyy, #{fmt % [t.to_i, (t + 20).to_i]}, #{fmt % [t.to_i, (t - 20).to_i]}"
|
139
|
+
r.valid_etag?.should_be true
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
context "Caching#expiry_etag" do
|
144
|
+
specify "should return an expiry etag with the stamp and expiration time" do
|
145
|
+
r = DummyRequest.new
|
146
|
+
|
147
|
+
t = Time.now
|
148
|
+
fmt = Caching::EXPIRY_ETAG_FORMAT
|
149
|
+
max_age = 54321
|
150
|
+
|
151
|
+
r.expiry_etag(t, max_age).should == (fmt % [t.to_i, (t + max_age).to_i])
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
context "Caching#valid_stamp?" do
|
156
|
+
specify "should return nil if no If-Modified-Since header is included" do
|
157
|
+
r = DummyRequest.new
|
158
|
+
r.valid_stamp?(Time.now).should_be_nil
|
159
|
+
end
|
160
|
+
|
161
|
+
specify "should return nil if the If-Modified-Since header is different than the specified stamp" do
|
162
|
+
t = Time.now
|
163
|
+
r = DummyRequest.new
|
164
|
+
r.headers['If-Modified-Since'] = t.httpdate
|
165
|
+
r.valid_stamp?(t + 1).should_be_nil
|
166
|
+
r.valid_stamp?(t - 1).should_be_nil
|
167
|
+
end
|
168
|
+
|
169
|
+
specify "should return true if the If-Modified-Since header matches the specified stamp" do
|
170
|
+
t = Time.now
|
171
|
+
r = DummyRequest.new
|
172
|
+
r.headers['If-Modified-Since'] = t.httpdate
|
173
|
+
r.valid_stamp?(t).should_be true
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
context "Caching#validate_cache" do
|
178
|
+
specify "should return nil if no validators are present" do
|
179
|
+
r = DummyRequest.new
|
180
|
+
r.validate_cache(Time.now, 360).should_be_nil
|
181
|
+
end
|
182
|
+
|
183
|
+
specify "should check for a stamp validator" do
|
184
|
+
r = DummyRequest.new
|
185
|
+
t = Time.now
|
186
|
+
|
187
|
+
r.headers['If-Modified-Since'] = t.httpdate
|
188
|
+
r.validate_cache(t + 1, 360).should_be_nil
|
189
|
+
r.validate_cache(t - 1, 360).should_be_nil
|
190
|
+
r.validate_cache(t, 360).should_be true
|
191
|
+
end
|
192
|
+
|
193
|
+
specify "should check for an etag validator" do
|
194
|
+
r = DummyRequest.new
|
195
|
+
t = Time.now
|
196
|
+
etag = 'abcdef'
|
197
|
+
|
198
|
+
r.validate_cache(t, 360, etag).should_be_nil
|
199
|
+
r.headers['If-None-Match'] = 'aaa-bbb'
|
200
|
+
r.validate_cache(t, 360, etag).should_be_nil
|
201
|
+
r.headers['If-None-Match'] = "aaa-bbb, #{etag}"
|
202
|
+
r.validate_cache(t, 360, etag).should_be true
|
203
|
+
r.headers['If-None-Match'] = '*'
|
204
|
+
r.validate_cache(t, 360, etag).should_be true
|
205
|
+
end
|
206
|
+
|
207
|
+
specify "should check for an expiry etag validator if etag is unspecified" do
|
208
|
+
r = DummyRequest.new
|
209
|
+
t = Time.now
|
210
|
+
fmt = Caching::EXPIRY_ETAG_FORMAT
|
211
|
+
|
212
|
+
r.headers['If-None-Match'] = 'aaa-bbb'
|
213
|
+
r.validate_cache(t, 360).should_be_nil
|
214
|
+
r.headers['If-None-Match'] = "aaa-bbb, #{fmt % [t.to_i, (t + 20).to_i]}"
|
215
|
+
r.validate_cache(t, 360).should_be true
|
216
|
+
r.headers['If-None-Match'] = '*'
|
217
|
+
r.validate_cache(t, 360).should_be true
|
218
|
+
end
|
219
|
+
|
220
|
+
specify "should set the response headers with caching info if request did not validate" do
|
221
|
+
r = DummyRequest.new
|
222
|
+
t = Time.now
|
223
|
+
r.validate_cache(t, 360, 'aaa-bbb', :public, 'Cookie')
|
224
|
+
r.response_headers['ETag'].should == '"aaa-bbb"'
|
225
|
+
r.response_headers['Last-Modified'].should == t.httpdate
|
226
|
+
r.response_headers['Expires'].should == ((t + 360).httpdate)
|
227
|
+
r.response_headers['Cache-Control'].should == :public
|
228
|
+
r.response_headers['Vary'].should == 'Cookie'
|
229
|
+
end
|
230
|
+
|
231
|
+
specify "should set an expiry etag if no etag is specified" do
|
232
|
+
r = DummyRequest.new
|
233
|
+
t = Time.now
|
234
|
+
fmt = Caching::EXPIRY_ETAG_FORMAT
|
235
|
+
r.validate_cache(t, 360)
|
236
|
+
r.response_headers['ETag'].should == (
|
237
|
+
"\"#{fmt % [t.to_i, (t + 360).to_i]}\"")
|
238
|
+
end
|
239
|
+
|
240
|
+
specify "should send a 304 response if the cache validates" do
|
241
|
+
r = DummyRequest.new
|
242
|
+
t = Time.now
|
243
|
+
fmt = Caching::EXPIRY_ETAG_FORMAT
|
244
|
+
|
245
|
+
r.headers['If-None-Match'] = "aaa-bbb, #{fmt % [t.to_i, (t + 20).to_i]}"
|
246
|
+
r.validate_cache(t, 360).should_be true
|
247
|
+
r.socket.rewind
|
248
|
+
resp = r.socket.read
|
249
|
+
resp.should_match /^HTTP\/1.1 304 Not Modified\r\n/
|
250
|
+
|
251
|
+
r = DummyRequest.new
|
252
|
+
t = Time.now
|
253
|
+
|
254
|
+
r.headers['If-Modified-Since'] = t.httpdate
|
255
|
+
r.validate_cache(t, 360).should_be true
|
256
|
+
r.socket.rewind
|
257
|
+
resp = r.socket.read
|
258
|
+
resp.should_match /^HTTP\/1.1 304 Not Modified\r\n/
|
259
|
+
end
|
260
|
+
|
261
|
+
specify "should not send anything if the cache doesn't validate" do
|
262
|
+
r = DummyRequest.new
|
263
|
+
t = Time.now
|
264
|
+
|
265
|
+
r.validate_cache(t, 360).should_be_nil
|
266
|
+
r.socket.rewind
|
267
|
+
resp = r.socket.read
|
268
|
+
resp.should_be_empty
|
269
|
+
end
|
270
|
+
|
271
|
+
specify "should not execute the given block if the cache validates" do
|
272
|
+
r = DummyRequest.new
|
273
|
+
t = Time.now
|
274
|
+
r.headers['If-Modified-Since'] = t.httpdate
|
275
|
+
proc {r.validate_cache(t, 360) {raise}}.should_not_raise
|
276
|
+
end
|
277
|
+
|
278
|
+
specify "should return the result of the given block if the cache doesn't validate" do
|
279
|
+
x = nil
|
280
|
+
l = proc {x = :executed}
|
281
|
+
|
282
|
+
r = DummyRequest.new
|
283
|
+
t = Time.now
|
284
|
+
r.validate_cache(t, 360, &l).should == :executed
|
285
|
+
x.should == :executed
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
context "Caching#send_not_modified_response" do
|
290
|
+
specify "should render a 304 response" do
|
291
|
+
r = DummyRequest.new
|
292
|
+
r.send_not_modified_response
|
293
|
+
r.socket.rewind
|
294
|
+
resp = r.socket.read
|
295
|
+
resp.should_match /^HTTP\/1.1 304 Not Modified\r\n/
|
296
|
+
resp.should_match /Content-Length: 0\r\n/
|
297
|
+
resp.should_match /\r\n\r\n$/ # empty response body
|
298
|
+
end
|
299
|
+
|
300
|
+
specify "should exclude Connection header if persistent" do
|
301
|
+
r = DummyRequest.new
|
302
|
+
r.persistent = true
|
303
|
+
r.send_not_modified_response
|
304
|
+
r.socket.rewind
|
305
|
+
resp = r.socket.read
|
306
|
+
resp.should_not_match /Connection: close\r\n/
|
307
|
+
end
|
308
|
+
|
309
|
+
specify "should include Connection header if persistent" do
|
310
|
+
r = DummyRequest.new
|
311
|
+
r.persistent = false
|
312
|
+
r.send_not_modified_response
|
313
|
+
r.socket.rewind
|
314
|
+
resp = r.socket.read
|
315
|
+
resp.should_match /Connection: close\r\n/
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../lib/serverside')
|
2
|
+
|
3
|
+
__END__
|
4
|
+
|
5
|
+
context "Daemon::Cluster::PidFile" do
|
6
|
+
setup do
|
7
|
+
@fn = Daemon::Cluster::PidFile::FN
|
8
|
+
end
|
9
|
+
|
10
|
+
specify "::FN should be the cluster's pid file" do
|
11
|
+
Daemon::Cluster::PidFile::FN.should == 'serverside_cluster.pid'
|
12
|
+
end
|
13
|
+
|
14
|
+
specify "should delete the cluster's pid file" do
|
15
|
+
FileUtils.touch(@fn)
|
16
|
+
File.file?(@fn).should == true
|
17
|
+
Daemon::Cluster::PidFile.delete
|
18
|
+
File.file?(@fn).should == false
|
19
|
+
end
|
20
|
+
|
21
|
+
specify "should store multiple pids" do
|
22
|
+
Daemon::Cluster::PidFile.delete
|
23
|
+
Daemon::Cluster::PidFile.store_pid(1111)
|
24
|
+
IO.read(@fn).should == "1111\n"
|
25
|
+
Daemon::Cluster::PidFile.store_pid(2222)
|
26
|
+
IO.read(@fn).should == "1111\n2222\n"
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_pid_recall_pids
|
30
|
+
Daemon::Cluster::PidFile.delete
|
31
|
+
proc {Daemon::Cluster::PidFile.recall_pids}.should_raise Errno::ENOENT
|
32
|
+
File.open(@fn, 'w') {|f| f.puts 3333; f.puts 4444}
|
33
|
+
Daemon::Cluster::PidFile.recall_pids.should == [3333, 4444]
|
34
|
+
|
35
|
+
FileUtils.rm(@fn)
|
36
|
+
Daemon::Cluster::PidFile.store_pid(6666)
|
37
|
+
Daemon::Cluster::PidFile.store_pid(7777)
|
38
|
+
Daemon::Cluster::PidFile.recall_pids.should == [6666, 7777]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class DummyCluster < Daemon::Cluster
|
43
|
+
FN = 'result'
|
44
|
+
|
45
|
+
def self.server_loop(port)
|
46
|
+
at_exit {File.open(FN, 'a') {|f| f.puts port}}
|
47
|
+
loop {sleep 10}
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.ports
|
51
|
+
5555..5556
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context "Cluster.fork_server" do
|
56
|
+
specify "should fork a server on the specified port" do
|
57
|
+
FileUtils.rm(DummyCluster::FN) rescue nil
|
58
|
+
port = rand(5_000)
|
59
|
+
pid = DummyCluster.fork_server(port)
|
60
|
+
sleep 1
|
61
|
+
Process.kill('TERM', pid)
|
62
|
+
sleep 0.1
|
63
|
+
File.file?(DummyCluster::FN).should == true
|
64
|
+
File.open(DummyCluster::FN, 'r') do |f|
|
65
|
+
f.gets.to_i.should == port
|
66
|
+
f.eof?.should == true
|
67
|
+
end
|
68
|
+
FileUtils.rm(DummyCluster::FN) rescue nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "Cluster.start_servers" do
|
73
|
+
specify "should start a cluster of servers" do
|
74
|
+
FileUtils.rm(DummyCluster::FN) rescue nil
|
75
|
+
DummyCluster.start_servers
|
76
|
+
sleep 0.5
|
77
|
+
pids = Daemon::Cluster::PidFile.recall_pids
|
78
|
+
pids.length.should == 2
|
79
|
+
pids.each {|pid| Process.kill('TERM', pid)}
|
80
|
+
sleep 0.5
|
81
|
+
File.open(DummyCluster::FN, 'r') do |f|
|
82
|
+
p1, p2 = f.gets.to_i, f.gets.to_i
|
83
|
+
DummyCluster.ports.include?(p1).should == true
|
84
|
+
DummyCluster.ports.include?(p2).should == true
|
85
|
+
p1.should_not == p2
|
86
|
+
f.eof?.should == true
|
87
|
+
end
|
88
|
+
FileUtils.rm(DummyCluster::FN) rescue nil
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context "Cluster.stop_servers" do
|
93
|
+
specify "should stop the cluster of servers" do
|
94
|
+
DummyCluster.start_servers
|
95
|
+
sleep 0.5
|
96
|
+
pids = Daemon::Cluster::PidFile.recall_pids
|
97
|
+
DummyCluster.stop_servers
|
98
|
+
sleep 0.5
|
99
|
+
File.file?(Daemon::Cluster::PidFile::FN).should == false
|
100
|
+
File.open(DummyCluster::FN, 'r') do |f|
|
101
|
+
p1, p2 = f.gets.to_i, f.gets.to_i
|
102
|
+
DummyCluster.ports.include?(p1).should == true
|
103
|
+
DummyCluster.ports.include?(p2).should == true
|
104
|
+
p1.should_not == p2
|
105
|
+
f.eof?.should == true
|
106
|
+
end
|
107
|
+
FileUtils.rm(DummyCluster::FN) rescue nil
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class DummyCluster2 < Daemon::Cluster
|
112
|
+
def self.daemon_loop
|
113
|
+
@@a = true
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.start_servers
|
117
|
+
@@b = true
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.stop_servers
|
121
|
+
@@c = true
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.a; @@a; end
|
125
|
+
def self.b; @@b; end
|
126
|
+
def self.c; @@c; end
|
127
|
+
end
|
128
|
+
|
129
|
+
context "Cluster.start and stop" do
|
130
|
+
specify "should start and stop the cluster daemon" do
|
131
|
+
DummyCluster2.start
|
132
|
+
DummyCluster2.a.should == true
|
133
|
+
DummyCluster2.b.should == true
|
134
|
+
|
135
|
+
proc {DummyCluster2.c}.should_raise
|
136
|
+
DummyCluster2.stop
|
137
|
+
DummyCluster2.c.should == true
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require File.join(File.dirname(__FILE__), '
|
1
|
+
require File.join(File.dirname(__FILE__), '../lib/serverside')
|
2
2
|
|
3
3
|
class ServerSide::HTTP::Connection
|
4
4
|
attr_reader :socket, :request_class, :thread
|
@@ -49,11 +49,11 @@ context "Connection.initialize" do
|
|
49
49
|
$pause_request = true
|
50
50
|
c = Connection.new(DummySocket.new, DummyRequest1)
|
51
51
|
c.thread.should_be_an_instance_of Thread
|
52
|
-
c.thread.alive?.
|
53
|
-
DummyRequest1.instance_count.
|
52
|
+
c.thread.alive?.should == true
|
53
|
+
DummyRequest1.instance_count.should == 1
|
54
54
|
$pause_request = false
|
55
55
|
sleep 0.1 while c.thread.alive?
|
56
|
-
DummyRequest1.instance_count.
|
56
|
+
DummyRequest1.instance_count.should == 1000
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require File.join(File.dirname(__FILE__), '
|
1
|
+
require File.join(File.dirname(__FILE__), '../lib/serverside')
|
2
2
|
require 'stringio'
|
3
3
|
|
4
4
|
class ServerSide::Router
|
@@ -28,14 +28,15 @@ context "ServerSide::Controller.mount" do
|
|
28
28
|
ServerSide::Router.reset_rules
|
29
29
|
rule = {:path => '/test'}
|
30
30
|
c = ServerSide::Controller.mount(rule)
|
31
|
+
sub_class = Class.new(c)
|
31
32
|
r = ServerSide::Router.rules.first
|
32
|
-
r.first.
|
33
|
+
r.first.should == rule
|
33
34
|
r.last.should_be_a_kind_of Proc
|
34
35
|
c.module_eval do
|
35
36
|
define_method(:initialize) {|req| $req = req}
|
36
37
|
end
|
37
38
|
res = r.last.call
|
38
|
-
res.should_be_a_kind_of
|
39
|
+
res.should_be_a_kind_of sub_class
|
39
40
|
|
40
41
|
r = ServerSide::Router.new(StringIO.new)
|
41
42
|
r.path = '/test'
|
@@ -46,13 +47,13 @@ context "ServerSide::Controller.mount" do
|
|
46
47
|
specify "should accept either an argument or block as the rule" do
|
47
48
|
ServerSide::Router.reset_rules
|
48
49
|
rule = {:path => '/test'}
|
49
|
-
c = ServerSide::Controller.mount(rule)
|
50
|
+
c = Class.new(ServerSide::Controller.mount(rule))
|
50
51
|
r = ServerSide::Router.rules.first
|
51
52
|
r.first.should_be rule
|
52
53
|
|
53
54
|
ServerSide::Router.reset_rules
|
54
55
|
rule = proc {true}
|
55
|
-
c = ServerSide::Controller.mount(&rule)
|
56
|
+
c = Class.new(ServerSide::Controller.mount(&rule))
|
56
57
|
r = ServerSide::Router.rules.first
|
57
58
|
r.first.should_be rule
|
58
59
|
end
|
@@ -69,10 +70,10 @@ end
|
|
69
70
|
require 'metaid'
|
70
71
|
|
71
72
|
class DummyController < ServerSide::Controller
|
72
|
-
attr_reader :
|
73
|
+
attr_reader :response_called
|
73
74
|
|
74
|
-
def
|
75
|
-
@
|
75
|
+
def response
|
76
|
+
@response_called = true
|
76
77
|
end
|
77
78
|
|
78
79
|
def render_default
|
@@ -91,10 +92,10 @@ context "ServerSide::Controller new instance" do
|
|
91
92
|
c.parameters.should_be req.parameters
|
92
93
|
end
|
93
94
|
|
94
|
-
specify "should invoke the
|
95
|
+
specify "should invoke the response method" do
|
95
96
|
req = ServerSide::HTTP::Request.new(StringIO.new)
|
96
97
|
c = DummyController.new(req)
|
97
|
-
c.
|
98
|
+
c.response_called.should_be true
|
98
99
|
end
|
99
100
|
|
100
101
|
specify "should invoke render_default unless @rendered" do
|
@@ -103,7 +104,7 @@ context "ServerSide::Controller new instance" do
|
|
103
104
|
c.rendered.should_be :default
|
104
105
|
|
105
106
|
c_class = Class.new(DummyController) do
|
106
|
-
define_method(:
|
107
|
+
define_method(:response) {@rendered = true}
|
107
108
|
end
|
108
109
|
c = c_class.new(req)
|
109
110
|
c.rendered.should_be true
|
@@ -134,6 +135,8 @@ context "ServerSide::Controller.render" do
|
|
134
135
|
req = ServerSide::HTTP::Request.new(StringIO.new)
|
135
136
|
c = ServerSide::Controller.new(req)
|
136
137
|
c.render('hello world', 'text/plain')
|
137
|
-
c.rendered.
|
138
|
+
c.rendered.should == true
|
138
139
|
end
|
139
140
|
end
|
141
|
+
|
142
|
+
|