rack-cache 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rack-cache might be problematic. Click here for more details.
- data/CHANGES +27 -0
- data/COPYING +18 -0
- data/README +96 -0
- data/Rakefile +144 -0
- data/TODO +40 -0
- data/doc/configuration.markdown +224 -0
- data/doc/events.dot +27 -0
- data/doc/faq.markdown +133 -0
- data/doc/index.markdown +113 -0
- data/doc/layout.html.erb +33 -0
- data/doc/license.markdown +24 -0
- data/doc/rack-cache.css +362 -0
- data/doc/storage.markdown +162 -0
- data/lib/rack/cache.rb +51 -0
- data/lib/rack/cache/config.rb +65 -0
- data/lib/rack/cache/config/busters.rb +16 -0
- data/lib/rack/cache/config/default.rb +134 -0
- data/lib/rack/cache/config/no-cache.rb +13 -0
- data/lib/rack/cache/context.rb +95 -0
- data/lib/rack/cache/core.rb +271 -0
- data/lib/rack/cache/entitystore.rb +224 -0
- data/lib/rack/cache/headers.rb +237 -0
- data/lib/rack/cache/metastore.rb +309 -0
- data/lib/rack/cache/options.rb +119 -0
- data/lib/rack/cache/request.rb +37 -0
- data/lib/rack/cache/response.rb +76 -0
- data/lib/rack/cache/storage.rb +50 -0
- data/lib/rack/utils/environment_headers.rb +78 -0
- data/rack-cache.gemspec +74 -0
- data/test/cache_test.rb +35 -0
- data/test/config_test.rb +66 -0
- data/test/context_test.rb +465 -0
- data/test/core_test.rb +84 -0
- data/test/entitystore_test.rb +176 -0
- data/test/environment_headers_test.rb +71 -0
- data/test/headers_test.rb +215 -0
- data/test/logging_test.rb +45 -0
- data/test/metastore_test.rb +210 -0
- data/test/options_test.rb +64 -0
- data/test/pony.jpg +0 -0
- data/test/response_test.rb +37 -0
- data/test/spec_setup.rb +189 -0
- data/test/storage_test.rb +94 -0
- metadata +120 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
|
+
require 'rack/cache/context'
|
3
|
+
|
4
|
+
describe "Rack::Cache::Context logging" do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
respond_with 200
|
8
|
+
@errors = StringIO.new
|
9
|
+
@cache = Rack::Cache::Context.new(@app)
|
10
|
+
@cache.errors = @errors
|
11
|
+
@cache.metaclass.send :public, :log, :trace, :warn, :info
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'responds to #log by writing message to #errors' do
|
15
|
+
@cache.log :test, 'is this thing on?'
|
16
|
+
@errors.string.should.be == "[cache] test: is this thing on?\n"
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'allows printf formatting arguments' do
|
20
|
+
@cache.log :test, '%s %p %i %x', 'hello', 'goodbye', 42, 66
|
21
|
+
@errors.string.should.be == "[cache] test: hello \"goodbye\" 42 42\n"
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'responds to #info by logging an :info message' do
|
25
|
+
@cache.info 'informative stuff'
|
26
|
+
@errors.string.should.be == "[cache] info: informative stuff\n"
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'responds to #warn by logging an :warn message' do
|
30
|
+
@cache.warn 'kinda/maybe bad stuff'
|
31
|
+
@errors.string.should.be == "[cache] warn: kinda/maybe bad stuff\n"
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'responds to #trace by logging a :trace message' do
|
35
|
+
@cache.trace 'some insignifacant event'
|
36
|
+
@errors.string.should.be == "[cache] trace: some insignifacant event\n"
|
37
|
+
end
|
38
|
+
|
39
|
+
it "doesn't log trace messages when not in verbose mode" do
|
40
|
+
@cache.verbose = false
|
41
|
+
@cache.trace 'some insignifacant event'
|
42
|
+
@errors.string.should.be == ""
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
|
+
require 'rack/cache/metastore'
|
3
|
+
|
4
|
+
describe_shared 'A Rack::Cache::MetaStore Implementation' do
|
5
|
+
|
6
|
+
before do
|
7
|
+
@request = mock_request('/', {})
|
8
|
+
@response = mock_response(200, {}, ['hello world'])
|
9
|
+
@entity_store = nil
|
10
|
+
end
|
11
|
+
after do
|
12
|
+
@store = nil
|
13
|
+
@entity_store = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# Low-level implementation methods ===========================================
|
17
|
+
|
18
|
+
it 'writes a list of negotation tuples with #write' do
|
19
|
+
lambda { @store.write('/test', [[{}, {}]]) }.should.not.raise
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'reads a list of negotation tuples with #read' do
|
23
|
+
@store.write('/test', [[{},{}],[{},{}]])
|
24
|
+
tuples = @store.read('/test')
|
25
|
+
tuples.should.be == [ [{},{}], [{},{}] ]
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'reads an empty list with #read when nothing cached at key' do
|
29
|
+
@store.read('/nothing').should.be.empty
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'removes entries for key with #purge' do
|
33
|
+
@store.write('/test', [[{},{}]])
|
34
|
+
@store.read('/test').should.not.be.empty
|
35
|
+
|
36
|
+
@store.purge('/test')
|
37
|
+
@store.read('/test').should.be.empty
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'succeeds when purging non-existing entries' do
|
41
|
+
@store.read('/test').should.be.empty
|
42
|
+
@store.purge('/test')
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'returns nil from #purge' do
|
46
|
+
@store.write('/test', [[{},{}]])
|
47
|
+
@store.purge('/test').should.be nil
|
48
|
+
@store.read('/test').should.be == []
|
49
|
+
end
|
50
|
+
|
51
|
+
%w[/test http://example.com:8080/ /test?x=y /test?x=y&p=q].each do |key|
|
52
|
+
it "can read and write key: '#{key}'" do
|
53
|
+
lambda { @store.write(key, [[{},{}]]) }.should.not.raise
|
54
|
+
@store.read(key).should.be == [[{},{}]]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
it "can read and write fairly large keys" do
|
59
|
+
key = "b" * 4096
|
60
|
+
lambda { @store.write(key, [[{},{}]]) }.should.not.raise
|
61
|
+
@store.read(key).should.be == [[{},{}]]
|
62
|
+
end
|
63
|
+
|
64
|
+
# Abstract methods ===========================================================
|
65
|
+
|
66
|
+
define_method :store_simple_entry do
|
67
|
+
@request = mock_request('/test', {})
|
68
|
+
@response = mock_response(200, {'Cache-Control' => 'max-age=420'}, ['test'])
|
69
|
+
body = @response.body
|
70
|
+
@store.store(@request, @response, @entity_store)
|
71
|
+
@response.body.should.not.be body
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'stores a cache entry' do
|
75
|
+
store_simple_entry
|
76
|
+
@store.read('/test').should.not.be.empty
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'sets the X-Content-Digest response header before storing' do
|
80
|
+
store_simple_entry
|
81
|
+
req, res = @store.read('/test').first
|
82
|
+
res['X-Content-Digest'].should.be == 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'finds a stored entry with #lookup' do
|
86
|
+
store_simple_entry
|
87
|
+
response = @store.lookup(@request, @entity_store)
|
88
|
+
response.should.not.be.nil
|
89
|
+
response.should.be.kind_of Rack::Cache::Response
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'restores response headers properly with #lookup' do
|
93
|
+
store_simple_entry
|
94
|
+
response = @store.lookup(@request, @entity_store)
|
95
|
+
response.headers.reject{|k,v| k =~ /^X-/}.
|
96
|
+
should.be == @response.headers.merge('Age' => '0', 'Content-Length' => '4')
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'restores response body from entity store with #lookup' do
|
100
|
+
store_simple_entry
|
101
|
+
response = @store.lookup(@request, @entity_store)
|
102
|
+
body = '' ; response.body.each {|p| body << p}
|
103
|
+
body.should.be == 'test'
|
104
|
+
end
|
105
|
+
|
106
|
+
# Vary =======================================================================
|
107
|
+
|
108
|
+
it 'does not return entries that Vary with #lookup' do
|
109
|
+
req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
|
110
|
+
req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
|
111
|
+
res = mock_response(200, {'Vary' => 'Foo Bar'}, ['test'])
|
112
|
+
@store.store(req1, res, @entity_store)
|
113
|
+
|
114
|
+
@store.lookup(req2, @entity_store).should.be.nil
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'stores multiple responses for each Vary combination' do
|
118
|
+
req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
|
119
|
+
res1 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 1'])
|
120
|
+
@store.store(req1, res1, @entity_store)
|
121
|
+
|
122
|
+
req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
|
123
|
+
res2 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 2'])
|
124
|
+
@store.store(req2, res2, @entity_store)
|
125
|
+
|
126
|
+
req3 = mock_request('/test', {'HTTP_FOO' => 'Baz', 'HTTP_BAR' => 'Boom'})
|
127
|
+
res3 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 3'])
|
128
|
+
@store.store(req3, res3, @entity_store)
|
129
|
+
|
130
|
+
slurp(@store.lookup(req3, @entity_store).body).should.be == 'test 3'
|
131
|
+
slurp(@store.lookup(req1, @entity_store).body).should.be == 'test 1'
|
132
|
+
slurp(@store.lookup(req2, @entity_store).body).should.be == 'test 2'
|
133
|
+
|
134
|
+
@store.read('/test').length.should.be == 3
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'overwrites non-varying responses with #store' do
|
138
|
+
req1 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
|
139
|
+
res1 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 1'])
|
140
|
+
@store.store(req1, res1, @entity_store)
|
141
|
+
slurp(@store.lookup(req1, @entity_store).body).should.be == 'test 1'
|
142
|
+
|
143
|
+
req2 = mock_request('/test', {'HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'})
|
144
|
+
res2 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 2'])
|
145
|
+
@store.store(req2, res2, @entity_store)
|
146
|
+
slurp(@store.lookup(req2, @entity_store).body).should.be == 'test 2'
|
147
|
+
|
148
|
+
req3 = mock_request('/test', {'HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'})
|
149
|
+
res3 = mock_response(200, {'Vary' => 'Foo Bar'}, ['test 3'])
|
150
|
+
@store.store(req3, res3, @entity_store)
|
151
|
+
slurp(@store.lookup(req1, @entity_store).body).should.be == 'test 3'
|
152
|
+
|
153
|
+
@store.read('/test').length.should.be == 2
|
154
|
+
end
|
155
|
+
|
156
|
+
# Helper Methods =============================================================
|
157
|
+
|
158
|
+
define_method :mock_request do |uri,opts|
|
159
|
+
env = Rack::MockRequest.env_for(uri, opts || {})
|
160
|
+
Rack::Cache::Request.new(env)
|
161
|
+
end
|
162
|
+
|
163
|
+
define_method :mock_response do |status,headers,body|
|
164
|
+
headers ||= {}
|
165
|
+
body = Array(body).compact
|
166
|
+
Rack::Cache::Response.new(status, headers, body)
|
167
|
+
end
|
168
|
+
|
169
|
+
define_method :slurp do |body|
|
170
|
+
buf = ''
|
171
|
+
body.each {|part| buf << part }
|
172
|
+
buf
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
describe 'Rack::Cache::MetaStore' do
|
179
|
+
describe 'Heap' do
|
180
|
+
it_should_behave_like 'A Rack::Cache::MetaStore Implementation'
|
181
|
+
before do
|
182
|
+
@store = Rack::Cache::MetaStore::Heap.new
|
183
|
+
@entity_store = Rack::Cache::EntityStore::Heap.new
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe 'Disk' do
|
188
|
+
it_should_behave_like 'A Rack::Cache::MetaStore Implementation'
|
189
|
+
before do
|
190
|
+
@temp_dir = create_temp_directory
|
191
|
+
@store = Rack::Cache::MetaStore::Disk.new("#{@temp_dir}/meta")
|
192
|
+
@entity_store = Rack::Cache::EntityStore::Disk.new("#{@temp_dir}/entity")
|
193
|
+
end
|
194
|
+
after do
|
195
|
+
remove_entry_secure @temp_dir
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
need_memcached 'metastore tests' do
|
200
|
+
describe 'MemCache' do
|
201
|
+
it_should_behave_like 'A Rack::Cache::MetaStore Implementation'
|
202
|
+
before :each do
|
203
|
+
@temp_dir = create_temp_directory
|
204
|
+
$memcached.flush
|
205
|
+
@store = Rack::Cache::MetaStore::MemCache.new($memcached)
|
206
|
+
@entity_store = Rack::Cache::EntityStore::Heap.new
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
|
+
require 'rack/cache/options'
|
3
|
+
|
4
|
+
module Rack::Cache::Options
|
5
|
+
option_accessor :foo
|
6
|
+
end
|
7
|
+
|
8
|
+
class MockOptions
|
9
|
+
include Rack::Cache::Options
|
10
|
+
alias_method :initialize, :initialize_options
|
11
|
+
end
|
12
|
+
|
13
|
+
describe 'Rack::Cache::Options' do
|
14
|
+
before { @options = MockOptions.new }
|
15
|
+
|
16
|
+
describe '#set' do
|
17
|
+
it 'sets a Symbol option as rack-cache.symbol' do
|
18
|
+
@options.set :bar, 'baz'
|
19
|
+
@options.options['rack-cache.bar'].should.be == 'baz'
|
20
|
+
end
|
21
|
+
it 'sets a String option as string' do
|
22
|
+
@options.set 'foo.bar', 'bling'
|
23
|
+
@options.options['foo.bar'].should.be == 'bling'
|
24
|
+
end
|
25
|
+
it 'sets all key/value pairs when given a Hash' do
|
26
|
+
@options.set :foo => 'bar', :bar => 'baz', 'foo.bar' => 'bling'
|
27
|
+
@options.foo.should.be == 'bar'
|
28
|
+
@options.options['rack-cache.bar'].should.be == 'baz'
|
29
|
+
@options.options['foo.bar'].should.be == 'bling'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'makes options declared with option_accessor available as attributes' do
|
34
|
+
@options.set :foo, 'bar'
|
35
|
+
@options.foo.should.be == 'bar'
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'allows setting multiple options via assignment' do
|
39
|
+
@options.options = { :foo => 'bar', :bar => 'baz', 'foo.bar' => 'bling' }
|
40
|
+
@options.foo.should.be == 'bar'
|
41
|
+
@options.options['foo.bar'].should.be == 'bling'
|
42
|
+
@options.options['rack-cache.bar'].should.be == 'baz'
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'allows the meta store to be configured' do
|
46
|
+
@options.should.respond_to :metastore
|
47
|
+
@options.should.respond_to :metastore=
|
48
|
+
@options.metastore.should.not.be nil
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'allows the entity store to be configured' do
|
52
|
+
@options.should.respond_to :entitystore
|
53
|
+
@options.should.respond_to :entitystore=
|
54
|
+
@options.entitystore.should.not.be nil
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'allows log verbosity to be configured' do
|
58
|
+
@options.should.respond_to :verbose
|
59
|
+
@options.should.respond_to :verbose=
|
60
|
+
@options.should.respond_to :verbose?
|
61
|
+
@options.verbose.should.not.be.nil
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
data/test/pony.jpg
ADDED
Binary file
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_setup"
|
2
|
+
|
3
|
+
describe 'Rack::Cache::Response' do
|
4
|
+
|
5
|
+
before(:each) {
|
6
|
+
@now = Time.now
|
7
|
+
@response = Rack::Cache::Response.new(200, {'Date' => @now.httpdate}, '')
|
8
|
+
@one_hour_ago = Time.httpdate((Time.now - (60**2)).httpdate)
|
9
|
+
}
|
10
|
+
|
11
|
+
after(:each) {
|
12
|
+
@now, @response, @one_hour_ago = nil
|
13
|
+
}
|
14
|
+
|
15
|
+
it 'responds to cache-related methods' do
|
16
|
+
@response.should.respond_to :ttl
|
17
|
+
@response.should.respond_to :age
|
18
|
+
@response.should.respond_to :date
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'responds to #to_a with a Rack response tuple' do
|
22
|
+
@response.should.respond_to :to_a
|
23
|
+
@response.to_a.should.be == [200, {'Date' => @now.httpdate}, '']
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'retrieves headers with #[]' do
|
27
|
+
@response.headers['X-Foo'] = 'bar'
|
28
|
+
@response.should.respond_to :[]
|
29
|
+
@response['X-Foo'].should.be == 'bar'
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'sets headers with #[]=' do
|
33
|
+
@response.should.respond_to :[]=
|
34
|
+
@response['X-Foo'] = 'bar'
|
35
|
+
@response.headers['X-Foo'].should.be == 'bar'
|
36
|
+
end
|
37
|
+
end
|
data/test/spec_setup.rb
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'tmpdir'
|
3
|
+
|
4
|
+
[ STDOUT, STDERR ].each { |io| io.sync = true }
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'test/spec'
|
8
|
+
rescue LoadError => boom
|
9
|
+
require 'rubygems' rescue nil
|
10
|
+
require 'test/spec'
|
11
|
+
end
|
12
|
+
|
13
|
+
# Set the MEMCACHED environment variable as follows to enable testing
|
14
|
+
# of the MemCached meta and entity stores.
|
15
|
+
ENV['MEMCACHED'] ||= 'localhost:11215'
|
16
|
+
$memcached = nil
|
17
|
+
|
18
|
+
def have_memcached?(server=ENV['MEMCACHED'])
|
19
|
+
return true if $memcached
|
20
|
+
require 'memcached'
|
21
|
+
$memcached = Memcached.new(server)
|
22
|
+
$memcached.set('ping', '')
|
23
|
+
true
|
24
|
+
rescue LoadError => boom
|
25
|
+
$memcached = nil
|
26
|
+
false
|
27
|
+
rescue => boom
|
28
|
+
$memcached = nil
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
def need_memcached(forwhat)
|
33
|
+
if have_memcached?
|
34
|
+
yield
|
35
|
+
else
|
36
|
+
STDERR.puts "skipping memcached #{forwhat} (MEMCACHED environment variable not set)"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Setup the load path ..
|
41
|
+
$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__)) + '/lib'
|
42
|
+
$LOAD_PATH.unshift File.dirname(__FILE__)
|
43
|
+
|
44
|
+
require 'rack/cache'
|
45
|
+
|
46
|
+
|
47
|
+
# Methods for constructing downstream applications / response
|
48
|
+
# generators.
|
49
|
+
module CacheContextHelpers
|
50
|
+
|
51
|
+
# The Rack::Cache::Context instance used for the most recent
|
52
|
+
# request.
|
53
|
+
attr_reader :cache
|
54
|
+
|
55
|
+
# An Array of Rack::Cache::Context instances used for each request, in
|
56
|
+
# request order.
|
57
|
+
attr_reader :caches
|
58
|
+
|
59
|
+
# The Rack::Response instance result of the most recent request.
|
60
|
+
attr_reader :response
|
61
|
+
|
62
|
+
# An Array of Rack::Response instances for each request, in request order.
|
63
|
+
attr_reader :responses
|
64
|
+
|
65
|
+
# The backend application object.
|
66
|
+
attr_reader :app
|
67
|
+
|
68
|
+
def setup_cache_context
|
69
|
+
# holds each Rack::Cache::Context
|
70
|
+
@app = nil
|
71
|
+
|
72
|
+
# each time a request is made, a clone of @cache_template is used
|
73
|
+
# and appended to @caches.
|
74
|
+
@cache_template = nil
|
75
|
+
@cache = nil
|
76
|
+
@caches = []
|
77
|
+
@errors = StringIO.new
|
78
|
+
@cache_config = nil
|
79
|
+
|
80
|
+
@called = false
|
81
|
+
@request = nil
|
82
|
+
@response = nil
|
83
|
+
@responses = []
|
84
|
+
|
85
|
+
@storage = Rack::Cache::Storage.new
|
86
|
+
end
|
87
|
+
|
88
|
+
def teardown_cache_context
|
89
|
+
@app, @cache_template, @cache, @caches, @called,
|
90
|
+
@request, @response, @responses, @cache_config = nil
|
91
|
+
end
|
92
|
+
|
93
|
+
# A basic response with 200 status code and a tiny body.
|
94
|
+
def respond_with(status=200, headers={}, body=['Hello World'])
|
95
|
+
called = false
|
96
|
+
@app =
|
97
|
+
lambda do |env|
|
98
|
+
called = true
|
99
|
+
response = Rack::Response.new(body, status, headers)
|
100
|
+
request = Rack::Request.new(env)
|
101
|
+
yield request, response if block_given?
|
102
|
+
response.finish
|
103
|
+
end
|
104
|
+
@app.meta_def(:called?) { called }
|
105
|
+
@app.meta_def(:reset!) { called = false }
|
106
|
+
@app
|
107
|
+
end
|
108
|
+
|
109
|
+
def cache_config(&block)
|
110
|
+
@cache_config = block
|
111
|
+
end
|
112
|
+
|
113
|
+
def request(method, uri='/', opts={})
|
114
|
+
opts = {
|
115
|
+
'rack.run_once' => true,
|
116
|
+
'rack.errors' => @errors,
|
117
|
+
'rack-cache.storage' => @storage
|
118
|
+
}.merge(opts)
|
119
|
+
|
120
|
+
fail 'response not specified (use respond_with)' if @app.nil?
|
121
|
+
@app.reset! if @app.respond_to?(:reset!)
|
122
|
+
|
123
|
+
@cache_prototype ||= Rack::Cache::Context.new(@app, &@cache_config)
|
124
|
+
@cache = @cache_prototype.clone
|
125
|
+
@caches << @cache
|
126
|
+
@request = Rack::MockRequest.new(@cache)
|
127
|
+
yield @cache if block_given?
|
128
|
+
@response = @request.send(method, uri, opts)
|
129
|
+
@responses << @response
|
130
|
+
@response
|
131
|
+
end
|
132
|
+
|
133
|
+
def get(stem, env={}, &b)
|
134
|
+
request(:get, stem, env, &b)
|
135
|
+
end
|
136
|
+
|
137
|
+
def post(*args, &b)
|
138
|
+
request(:post, *args, &b)
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
module TestHelpers
|
145
|
+
include FileUtils
|
146
|
+
F = File
|
147
|
+
|
148
|
+
@@temp_dir_count = 0
|
149
|
+
|
150
|
+
def create_temp_directory
|
151
|
+
@@temp_dir_count += 1
|
152
|
+
path = F.join(Dir.tmpdir, "rcl-#{$$}-#{@@temp_dir_count}")
|
153
|
+
mkdir_p path
|
154
|
+
if block_given?
|
155
|
+
yield path
|
156
|
+
remove_entry_secure path
|
157
|
+
end
|
158
|
+
path
|
159
|
+
end
|
160
|
+
|
161
|
+
def create_temp_file(root, file, data='')
|
162
|
+
path = F.join(root, file)
|
163
|
+
mkdir_p F.dirname(path)
|
164
|
+
F.open(path, 'w') { |io| io.write(data) }
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
class Test::Unit::TestCase
|
170
|
+
include TestHelpers
|
171
|
+
include CacheContextHelpers
|
172
|
+
end
|
173
|
+
|
174
|
+
# Metaid == a few simple metaclass helper
|
175
|
+
# (See http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html.)
|
176
|
+
class Object
|
177
|
+
# The hidden singleton lurks behind everyone
|
178
|
+
def metaclass; class << self; self; end; end
|
179
|
+
def meta_eval(&blk); metaclass.instance_eval(&blk); end
|
180
|
+
# Adds methods to a metaclass
|
181
|
+
def meta_def name, &blk
|
182
|
+
meta_eval { define_method name, &blk }
|
183
|
+
end
|
184
|
+
# Defines an instance method within a class
|
185
|
+
def class_def name, &blk
|
186
|
+
class_eval { define_method name, &blk }
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|