lack 2.0.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.
- checksums.yaml +7 -0
- data/bin/rackup +5 -0
- data/lib/rack.rb +26 -0
- data/lib/rack/body_proxy.rb +39 -0
- data/lib/rack/builder.rb +166 -0
- data/lib/rack/handler.rb +63 -0
- data/lib/rack/handler/webrick.rb +120 -0
- data/lib/rack/mime.rb +661 -0
- data/lib/rack/mock.rb +198 -0
- data/lib/rack/multipart.rb +31 -0
- data/lib/rack/multipart/generator.rb +93 -0
- data/lib/rack/multipart/parser.rb +239 -0
- data/lib/rack/multipart/uploaded_file.rb +34 -0
- data/lib/rack/request.rb +394 -0
- data/lib/rack/response.rb +160 -0
- data/lib/rack/server.rb +258 -0
- data/lib/rack/server/options.rb +121 -0
- data/lib/rack/utils.rb +653 -0
- data/lib/rack/version.rb +3 -0
- data/spec/spec_helper.rb +1 -0
- data/test/builder/anything.rb +5 -0
- data/test/builder/comment.ru +4 -0
- data/test/builder/end.ru +5 -0
- data/test/builder/line.ru +1 -0
- data/test/builder/options.ru +2 -0
- data/test/multipart/bad_robots +259 -0
- data/test/multipart/binary +0 -0
- data/test/multipart/content_type_and_no_filename +6 -0
- data/test/multipart/empty +10 -0
- data/test/multipart/fail_16384_nofile +814 -0
- data/test/multipart/file1.txt +1 -0
- data/test/multipart/filename_and_modification_param +7 -0
- data/test/multipart/filename_and_no_name +6 -0
- data/test/multipart/filename_with_escaped_quotes +6 -0
- data/test/multipart/filename_with_escaped_quotes_and_modification_param +7 -0
- data/test/multipart/filename_with_percent_escaped_quotes +6 -0
- data/test/multipart/filename_with_unescaped_percentages +6 -0
- data/test/multipart/filename_with_unescaped_percentages2 +6 -0
- data/test/multipart/filename_with_unescaped_percentages3 +6 -0
- data/test/multipart/filename_with_unescaped_quotes +6 -0
- data/test/multipart/ie +6 -0
- data/test/multipart/invalid_character +6 -0
- data/test/multipart/mixed_files +21 -0
- data/test/multipart/nested +10 -0
- data/test/multipart/none +9 -0
- data/test/multipart/semicolon +6 -0
- data/test/multipart/text +15 -0
- data/test/multipart/webkit +32 -0
- data/test/rackup/config.ru +31 -0
- data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
- data/test/spec_body_proxy.rb +69 -0
- data/test/spec_builder.rb +223 -0
- data/test/spec_chunked.rb +101 -0
- data/test/spec_file.rb +221 -0
- data/test/spec_handler.rb +59 -0
- data/test/spec_head.rb +45 -0
- data/test/spec_lint.rb +522 -0
- data/test/spec_mime.rb +51 -0
- data/test/spec_mock.rb +277 -0
- data/test/spec_multipart.rb +547 -0
- data/test/spec_recursive.rb +72 -0
- data/test/spec_request.rb +1199 -0
- data/test/spec_response.rb +343 -0
- data/test/spec_rewindable_input.rb +118 -0
- data/test/spec_sendfile.rb +130 -0
- data/test/spec_server.rb +167 -0
- data/test/spec_utils.rb +635 -0
- data/test/spec_webrick.rb +184 -0
- data/test/testrequest.rb +78 -0
- data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
- data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
- metadata +240 -0
@@ -0,0 +1,343 @@
|
|
1
|
+
require 'rack/response'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
describe Rack::Response do
|
5
|
+
should "have sensible default values" do
|
6
|
+
response = Rack::Response.new
|
7
|
+
status, header, body = response.finish
|
8
|
+
status.should.equal 200
|
9
|
+
header.should.equal({})
|
10
|
+
body.each { |part|
|
11
|
+
part.should.equal ""
|
12
|
+
}
|
13
|
+
|
14
|
+
response = Rack::Response.new
|
15
|
+
status, header, body = *response
|
16
|
+
status.should.equal 200
|
17
|
+
header.should.equal({})
|
18
|
+
body.each { |part|
|
19
|
+
part.should.equal ""
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
it "can be written to" do
|
24
|
+
response = Rack::Response.new
|
25
|
+
|
26
|
+
_, _, body = response.finish do
|
27
|
+
response.write "foo"
|
28
|
+
response.write "bar"
|
29
|
+
response.write "baz"
|
30
|
+
end
|
31
|
+
|
32
|
+
parts = []
|
33
|
+
body.each { |part| parts << part }
|
34
|
+
|
35
|
+
parts.should.equal ["foo", "bar", "baz"]
|
36
|
+
end
|
37
|
+
|
38
|
+
it "can set and read headers" do
|
39
|
+
response = Rack::Response.new
|
40
|
+
response["Content-Type"].should.equal nil
|
41
|
+
response["Content-Type"] = "text/plain"
|
42
|
+
response["Content-Type"].should.equal "text/plain"
|
43
|
+
end
|
44
|
+
|
45
|
+
it "can override the initial Content-Type with a different case" do
|
46
|
+
response = Rack::Response.new("", 200, "content-type" => "text/plain")
|
47
|
+
response["Content-Type"].should.equal "text/plain"
|
48
|
+
end
|
49
|
+
|
50
|
+
it "can set cookies" do
|
51
|
+
response = Rack::Response.new
|
52
|
+
|
53
|
+
response.set_cookie "foo", "bar"
|
54
|
+
response["Set-Cookie"].should.equal "foo=bar"
|
55
|
+
response.set_cookie "foo2", "bar2"
|
56
|
+
response["Set-Cookie"].should.equal ["foo=bar", "foo2=bar2"].join("\n")
|
57
|
+
response.set_cookie "foo3", "bar3"
|
58
|
+
response["Set-Cookie"].should.equal ["foo=bar", "foo2=bar2", "foo3=bar3"].join("\n")
|
59
|
+
end
|
60
|
+
|
61
|
+
it "can set cookies with the same name for multiple domains" do
|
62
|
+
response = Rack::Response.new
|
63
|
+
response.set_cookie "foo", {:value => "bar", :domain => "sample.example.com"}
|
64
|
+
response.set_cookie "foo", {:value => "bar", :domain => ".example.com"}
|
65
|
+
response["Set-Cookie"].should.equal ["foo=bar; domain=sample.example.com", "foo=bar; domain=.example.com"].join("\n")
|
66
|
+
end
|
67
|
+
|
68
|
+
it "formats the Cookie expiration date accordingly to RFC 6265" do
|
69
|
+
response = Rack::Response.new
|
70
|
+
|
71
|
+
response.set_cookie "foo", {:value => "bar", :expires => Time.now+10}
|
72
|
+
response["Set-Cookie"].should.match(
|
73
|
+
/expires=..., \d\d ... \d\d\d\d \d\d:\d\d:\d\d .../)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "can set secure cookies" do
|
77
|
+
response = Rack::Response.new
|
78
|
+
response.set_cookie "foo", {:value => "bar", :secure => true}
|
79
|
+
response["Set-Cookie"].should.equal "foo=bar; secure"
|
80
|
+
end
|
81
|
+
|
82
|
+
it "can set http only cookies" do
|
83
|
+
response = Rack::Response.new
|
84
|
+
response.set_cookie "foo", {:value => "bar", :httponly => true}
|
85
|
+
response["Set-Cookie"].should.equal "foo=bar; HttpOnly"
|
86
|
+
end
|
87
|
+
|
88
|
+
it "can set http only cookies with :http_only" do
|
89
|
+
response = Rack::Response.new
|
90
|
+
response.set_cookie "foo", {:value => "bar", :http_only => true}
|
91
|
+
response["Set-Cookie"].should.equal "foo=bar; HttpOnly"
|
92
|
+
end
|
93
|
+
|
94
|
+
it "can set prefers :httponly for http only cookie setting when :httponly and :http_only provided" do
|
95
|
+
response = Rack::Response.new
|
96
|
+
response.set_cookie "foo", {:value => "bar", :httponly => false, :http_only => true}
|
97
|
+
response["Set-Cookie"].should.equal "foo=bar"
|
98
|
+
end
|
99
|
+
|
100
|
+
it "can delete cookies" do
|
101
|
+
response = Rack::Response.new
|
102
|
+
response.set_cookie "foo", "bar"
|
103
|
+
response.set_cookie "foo2", "bar2"
|
104
|
+
response.delete_cookie "foo"
|
105
|
+
response["Set-Cookie"].should.equal [
|
106
|
+
"foo2=bar2",
|
107
|
+
"foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"
|
108
|
+
].join("\n")
|
109
|
+
end
|
110
|
+
|
111
|
+
it "can delete cookies with the same name from multiple domains" do
|
112
|
+
response = Rack::Response.new
|
113
|
+
response.set_cookie "foo", {:value => "bar", :domain => "sample.example.com"}
|
114
|
+
response.set_cookie "foo", {:value => "bar", :domain => ".example.com"}
|
115
|
+
response["Set-Cookie"].should.equal ["foo=bar; domain=sample.example.com", "foo=bar; domain=.example.com"].join("\n")
|
116
|
+
response.delete_cookie "foo", :domain => ".example.com"
|
117
|
+
response["Set-Cookie"].should.equal ["foo=bar; domain=sample.example.com", "foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"].join("\n")
|
118
|
+
response.delete_cookie "foo", :domain => "sample.example.com"
|
119
|
+
response["Set-Cookie"].should.equal ["foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000",
|
120
|
+
"foo=; domain=sample.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"].join("\n")
|
121
|
+
end
|
122
|
+
|
123
|
+
it "can delete cookies with the same name with different paths" do
|
124
|
+
response = Rack::Response.new
|
125
|
+
response.set_cookie "foo", {:value => "bar", :path => "/"}
|
126
|
+
response.set_cookie "foo", {:value => "bar", :path => "/path"}
|
127
|
+
response["Set-Cookie"].should.equal ["foo=bar; path=/",
|
128
|
+
"foo=bar; path=/path"].join("\n")
|
129
|
+
|
130
|
+
response.delete_cookie "foo", :path => "/path"
|
131
|
+
response["Set-Cookie"].should.equal ["foo=bar; path=/",
|
132
|
+
"foo=; path=/path; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"].join("\n")
|
133
|
+
end
|
134
|
+
|
135
|
+
it "can do redirects" do
|
136
|
+
response = Rack::Response.new
|
137
|
+
response.redirect "/foo"
|
138
|
+
status, header, body = response.finish
|
139
|
+
status.should.equal 302
|
140
|
+
header["Location"].should.equal "/foo"
|
141
|
+
|
142
|
+
response = Rack::Response.new
|
143
|
+
response.redirect "/foo", 307
|
144
|
+
status, header, body = response.finish
|
145
|
+
|
146
|
+
status.should.equal 307
|
147
|
+
end
|
148
|
+
|
149
|
+
it "has a useful constructor" do
|
150
|
+
r = Rack::Response.new("foo")
|
151
|
+
status, header, body = r.finish
|
152
|
+
str = ""; body.each { |part| str << part }
|
153
|
+
str.should.equal "foo"
|
154
|
+
|
155
|
+
r = Rack::Response.new(["foo", "bar"])
|
156
|
+
status, header, body = r.finish
|
157
|
+
str = ""; body.each { |part| str << part }
|
158
|
+
str.should.equal "foobar"
|
159
|
+
|
160
|
+
object_with_each = Object.new
|
161
|
+
def object_with_each.each
|
162
|
+
yield "foo"
|
163
|
+
yield "bar"
|
164
|
+
end
|
165
|
+
r = Rack::Response.new(object_with_each)
|
166
|
+
r.write "foo"
|
167
|
+
status, header, body = r.finish
|
168
|
+
str = ""; body.each { |part| str << part }
|
169
|
+
str.should.equal "foobarfoo"
|
170
|
+
|
171
|
+
r = Rack::Response.new([], 500)
|
172
|
+
r.status.should.equal 500
|
173
|
+
|
174
|
+
r = Rack::Response.new([], "200 OK")
|
175
|
+
r.status.should.equal 200
|
176
|
+
end
|
177
|
+
|
178
|
+
it "has a constructor that can take a block" do
|
179
|
+
r = Rack::Response.new { |res|
|
180
|
+
res.status = 404
|
181
|
+
res.write "foo"
|
182
|
+
}
|
183
|
+
status, _, body = r.finish
|
184
|
+
str = ""; body.each { |part| str << part }
|
185
|
+
str.should.equal "foo"
|
186
|
+
status.should.equal 404
|
187
|
+
end
|
188
|
+
|
189
|
+
it "doesn't return invalid responses" do
|
190
|
+
r = Rack::Response.new(["foo", "bar"], 204)
|
191
|
+
_, header, body = r.finish
|
192
|
+
str = ""; body.each { |part| str << part }
|
193
|
+
str.should.be.empty
|
194
|
+
header["Content-Type"].should.equal nil
|
195
|
+
header['Content-Length'].should.equal nil
|
196
|
+
|
197
|
+
lambda {
|
198
|
+
Rack::Response.new(Object.new)
|
199
|
+
}.should.raise(TypeError).
|
200
|
+
message.should =~ /stringable or iterable required/
|
201
|
+
end
|
202
|
+
|
203
|
+
it "knows if it's empty" do
|
204
|
+
r = Rack::Response.new
|
205
|
+
r.should.be.empty
|
206
|
+
r.write "foo"
|
207
|
+
r.should.not.be.empty
|
208
|
+
|
209
|
+
r = Rack::Response.new
|
210
|
+
r.should.be.empty
|
211
|
+
r.finish
|
212
|
+
r.should.be.empty
|
213
|
+
|
214
|
+
r = Rack::Response.new
|
215
|
+
r.should.be.empty
|
216
|
+
r.finish { }
|
217
|
+
r.should.not.be.empty
|
218
|
+
end
|
219
|
+
|
220
|
+
should "provide access to the HTTP status" do
|
221
|
+
res = Rack::Response.new
|
222
|
+
res.status = 200
|
223
|
+
res.should.be.successful
|
224
|
+
res.should.be.ok
|
225
|
+
|
226
|
+
res.status = 201
|
227
|
+
res.should.be.successful
|
228
|
+
res.should.be.created
|
229
|
+
|
230
|
+
res.status = 202
|
231
|
+
res.should.be.successful
|
232
|
+
res.should.be.accepted
|
233
|
+
|
234
|
+
res.status = 400
|
235
|
+
res.should.not.be.successful
|
236
|
+
res.should.be.client_error
|
237
|
+
res.should.be.bad_request
|
238
|
+
|
239
|
+
res.status = 401
|
240
|
+
res.should.not.be.successful
|
241
|
+
res.should.be.client_error
|
242
|
+
res.should.be.unauthorized
|
243
|
+
|
244
|
+
res.status = 404
|
245
|
+
res.should.not.be.successful
|
246
|
+
res.should.be.client_error
|
247
|
+
res.should.be.not_found
|
248
|
+
|
249
|
+
res.status = 405
|
250
|
+
res.should.not.be.successful
|
251
|
+
res.should.be.client_error
|
252
|
+
res.should.be.method_not_allowed
|
253
|
+
|
254
|
+
res.status = 418
|
255
|
+
res.should.not.be.successful
|
256
|
+
res.should.be.client_error
|
257
|
+
res.should.be.i_m_a_teapot
|
258
|
+
|
259
|
+
res.status = 422
|
260
|
+
res.should.not.be.successful
|
261
|
+
res.should.be.client_error
|
262
|
+
res.should.be.unprocessable
|
263
|
+
|
264
|
+
res.status = 501
|
265
|
+
res.should.not.be.successful
|
266
|
+
res.should.be.server_error
|
267
|
+
|
268
|
+
res.status = 307
|
269
|
+
res.should.be.redirect
|
270
|
+
end
|
271
|
+
|
272
|
+
should "provide access to the HTTP headers" do
|
273
|
+
res = Rack::Response.new
|
274
|
+
res["Content-Type"] = "text/yaml"
|
275
|
+
|
276
|
+
res.should.include "Content-Type"
|
277
|
+
res.headers["Content-Type"].should.equal "text/yaml"
|
278
|
+
res["Content-Type"].should.equal "text/yaml"
|
279
|
+
res.content_type.should.equal "text/yaml"
|
280
|
+
res.content_length.should.be.nil
|
281
|
+
res.location.should.be.nil
|
282
|
+
end
|
283
|
+
|
284
|
+
it "does not add or change Content-Length when #finish()ing" do
|
285
|
+
res = Rack::Response.new
|
286
|
+
res.status = 200
|
287
|
+
res.finish
|
288
|
+
res.headers["Content-Length"].should.be.nil
|
289
|
+
|
290
|
+
res = Rack::Response.new
|
291
|
+
res.status = 200
|
292
|
+
res.headers["Content-Length"] = "10"
|
293
|
+
res.finish
|
294
|
+
res.headers["Content-Length"].should.equal "10"
|
295
|
+
end
|
296
|
+
|
297
|
+
it "updates Content-Length when body appended to using #write" do
|
298
|
+
res = Rack::Response.new
|
299
|
+
res.status = 200
|
300
|
+
res.headers["Content-Length"].should.be.nil
|
301
|
+
res.write "Hi"
|
302
|
+
res.headers["Content-Length"].should.equal "2"
|
303
|
+
res.write " there"
|
304
|
+
res.headers["Content-Length"].should.equal "8"
|
305
|
+
end
|
306
|
+
|
307
|
+
it "calls close on #body" do
|
308
|
+
res = Rack::Response.new
|
309
|
+
res.body = StringIO.new
|
310
|
+
res.close
|
311
|
+
res.body.should.be.closed
|
312
|
+
end
|
313
|
+
|
314
|
+
it "calls close on #body when 204, 205, or 304" do
|
315
|
+
res = Rack::Response.new
|
316
|
+
res.body = StringIO.new
|
317
|
+
res.finish
|
318
|
+
res.body.should.not.be.closed
|
319
|
+
|
320
|
+
res.status = 204
|
321
|
+
_, _, b = res.finish
|
322
|
+
res.body.should.be.closed
|
323
|
+
b.should.not.equal res.body
|
324
|
+
|
325
|
+
res.body = StringIO.new
|
326
|
+
res.status = 205
|
327
|
+
_, _, b = res.finish
|
328
|
+
res.body.should.be.closed
|
329
|
+
b.should.not.equal res.body
|
330
|
+
|
331
|
+
res.body = StringIO.new
|
332
|
+
res.status = 304
|
333
|
+
_, _, b = res.finish
|
334
|
+
res.body.should.be.closed
|
335
|
+
b.should.not.equal res.body
|
336
|
+
end
|
337
|
+
|
338
|
+
it "wraps the body from #to_ary to prevent infinite loops" do
|
339
|
+
res = Rack::Response.new
|
340
|
+
res.finish.last.should.not.respond_to?(:to_ary)
|
341
|
+
lambda { res.finish.last.to_ary }.should.raise(NoMethodError)
|
342
|
+
end
|
343
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'rack/rewindable_input'
|
3
|
+
|
4
|
+
shared "a rewindable IO object" do
|
5
|
+
before do
|
6
|
+
@rio = Rack::RewindableInput.new(@io)
|
7
|
+
end
|
8
|
+
|
9
|
+
should "be able to handle to read()" do
|
10
|
+
@rio.read.should.equal "hello world"
|
11
|
+
end
|
12
|
+
|
13
|
+
should "be able to handle to read(nil)" do
|
14
|
+
@rio.read(nil).should.equal "hello world"
|
15
|
+
end
|
16
|
+
|
17
|
+
should "be able to handle to read(length)" do
|
18
|
+
@rio.read(1).should.equal "h"
|
19
|
+
end
|
20
|
+
|
21
|
+
should "be able to handle to read(length, buffer)" do
|
22
|
+
buffer = ""
|
23
|
+
result = @rio.read(1, buffer)
|
24
|
+
result.should.equal "h"
|
25
|
+
result.object_id.should.equal buffer.object_id
|
26
|
+
end
|
27
|
+
|
28
|
+
should "be able to handle to read(nil, buffer)" do
|
29
|
+
buffer = ""
|
30
|
+
result = @rio.read(nil, buffer)
|
31
|
+
result.should.equal "hello world"
|
32
|
+
result.object_id.should.equal buffer.object_id
|
33
|
+
end
|
34
|
+
|
35
|
+
should "rewind to the beginning when #rewind is called" do
|
36
|
+
@rio.read(1)
|
37
|
+
@rio.rewind
|
38
|
+
@rio.read.should.equal "hello world"
|
39
|
+
end
|
40
|
+
|
41
|
+
should "be able to handle gets" do
|
42
|
+
@rio.gets.should == "hello world"
|
43
|
+
end
|
44
|
+
|
45
|
+
should "be able to handle each" do
|
46
|
+
array = []
|
47
|
+
@rio.each do |data|
|
48
|
+
array << data
|
49
|
+
end
|
50
|
+
array.should.equal(["hello world"])
|
51
|
+
end
|
52
|
+
|
53
|
+
should "not buffer into a Tempfile if no data has been read yet" do
|
54
|
+
@rio.instance_variable_get(:@rewindable_io).should.be.nil
|
55
|
+
end
|
56
|
+
|
57
|
+
should "buffer into a Tempfile when data has been consumed for the first time" do
|
58
|
+
@rio.read(1)
|
59
|
+
tempfile = @rio.instance_variable_get(:@rewindable_io)
|
60
|
+
tempfile.should.not.be.nil
|
61
|
+
@rio.read(1)
|
62
|
+
tempfile2 = @rio.instance_variable_get(:@rewindable_io)
|
63
|
+
tempfile2.path.should == tempfile.path
|
64
|
+
end
|
65
|
+
|
66
|
+
should "close the underlying tempfile upon calling #close" do
|
67
|
+
@rio.read(1)
|
68
|
+
tempfile = @rio.instance_variable_get(:@rewindable_io)
|
69
|
+
@rio.close
|
70
|
+
tempfile.should.be.closed
|
71
|
+
end
|
72
|
+
|
73
|
+
should "be possible to call #close when no data has been buffered yet" do
|
74
|
+
lambda{ @rio.close }.should.not.raise
|
75
|
+
end
|
76
|
+
|
77
|
+
should "be possible to call #close multiple times" do
|
78
|
+
lambda{
|
79
|
+
@rio.close
|
80
|
+
@rio.close
|
81
|
+
}.should.not.raise
|
82
|
+
end
|
83
|
+
|
84
|
+
@rio.close
|
85
|
+
@rio = nil
|
86
|
+
end
|
87
|
+
|
88
|
+
describe Rack::RewindableInput do
|
89
|
+
describe "given an IO object that is already rewindable" do
|
90
|
+
before do
|
91
|
+
@io = StringIO.new("hello world")
|
92
|
+
end
|
93
|
+
|
94
|
+
behaves_like "a rewindable IO object"
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "given an IO object that is not rewindable" do
|
98
|
+
before do
|
99
|
+
@io = StringIO.new("hello world")
|
100
|
+
@io.instance_eval do
|
101
|
+
undef :rewind
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
behaves_like "a rewindable IO object"
|
106
|
+
end
|
107
|
+
|
108
|
+
describe "given an IO object whose rewind method raises Errno::ESPIPE" do
|
109
|
+
before do
|
110
|
+
@io = StringIO.new("hello world")
|
111
|
+
def @io.rewind
|
112
|
+
raise Errno::ESPIPE, "You can't rewind this!"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
behaves_like "a rewindable IO object"
|
117
|
+
end
|
118
|
+
end
|