rack-cache 0.2.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.
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
|
+
|