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.

Files changed (44) hide show
  1. data/CHANGES +27 -0
  2. data/COPYING +18 -0
  3. data/README +96 -0
  4. data/Rakefile +144 -0
  5. data/TODO +40 -0
  6. data/doc/configuration.markdown +224 -0
  7. data/doc/events.dot +27 -0
  8. data/doc/faq.markdown +133 -0
  9. data/doc/index.markdown +113 -0
  10. data/doc/layout.html.erb +33 -0
  11. data/doc/license.markdown +24 -0
  12. data/doc/rack-cache.css +362 -0
  13. data/doc/storage.markdown +162 -0
  14. data/lib/rack/cache.rb +51 -0
  15. data/lib/rack/cache/config.rb +65 -0
  16. data/lib/rack/cache/config/busters.rb +16 -0
  17. data/lib/rack/cache/config/default.rb +134 -0
  18. data/lib/rack/cache/config/no-cache.rb +13 -0
  19. data/lib/rack/cache/context.rb +95 -0
  20. data/lib/rack/cache/core.rb +271 -0
  21. data/lib/rack/cache/entitystore.rb +224 -0
  22. data/lib/rack/cache/headers.rb +237 -0
  23. data/lib/rack/cache/metastore.rb +309 -0
  24. data/lib/rack/cache/options.rb +119 -0
  25. data/lib/rack/cache/request.rb +37 -0
  26. data/lib/rack/cache/response.rb +76 -0
  27. data/lib/rack/cache/storage.rb +50 -0
  28. data/lib/rack/utils/environment_headers.rb +78 -0
  29. data/rack-cache.gemspec +74 -0
  30. data/test/cache_test.rb +35 -0
  31. data/test/config_test.rb +66 -0
  32. data/test/context_test.rb +465 -0
  33. data/test/core_test.rb +84 -0
  34. data/test/entitystore_test.rb +176 -0
  35. data/test/environment_headers_test.rb +71 -0
  36. data/test/headers_test.rb +215 -0
  37. data/test/logging_test.rb +45 -0
  38. data/test/metastore_test.rb +210 -0
  39. data/test/options_test.rb +64 -0
  40. data/test/pony.jpg +0 -0
  41. data/test/response_test.rb +37 -0
  42. data/test/spec_setup.rb +189 -0
  43. data/test/storage_test.rb +94 -0
  44. 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
@@ -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
+