rtomayko-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.
- data/CHANGES +50 -0
- data/COPYING +18 -0
- data/README +96 -0
- data/Rakefile +144 -0
- data/TODO +42 -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/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/config.rb +65 -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 +277 -0
- data/lib/rack/cache/metastore.rb +292 -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/cache.rb +51 -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 +505 -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 +222 -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 +122 -0
| @@ -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.
         | 
| 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 | 
            +
             | 
| @@ -0,0 +1,94 @@ | |
| 1 | 
            +
            require "#{File.dirname(__FILE__)}/spec_setup"
         | 
| 2 | 
            +
            require 'rack/cache/storage'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            describe 'Rack::Cache::Storage' do
         | 
| 5 | 
            +
              before do
         | 
| 6 | 
            +
                @storage = Rack::Cache::Storage.new
         | 
| 7 | 
            +
              end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              it "fails when an unknown URI scheme is provided" do
         | 
| 10 | 
            +
                lambda { @storage.resolve_metastore_uri('foo:/') }.should.raise
         | 
| 11 | 
            +
              end
         | 
| 12 | 
            +
              it "creates a new MetaStore for URI if none exists" do
         | 
| 13 | 
            +
                @storage.resolve_metastore_uri('heap:/').
         | 
| 14 | 
            +
                  should.be.kind_of Rack::Cache::MetaStore
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
              it "returns an existing MetaStore instance for URI that exists" do
         | 
| 17 | 
            +
                store = @storage.resolve_metastore_uri('heap:/')
         | 
| 18 | 
            +
                @storage.resolve_metastore_uri('heap:/').should.be store
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
              it "creates a new EntityStore for URI if none exists" do
         | 
| 21 | 
            +
                @storage.resolve_entitystore_uri('heap:/').
         | 
| 22 | 
            +
                  should.be.kind_of Rack::Cache::EntityStore
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
              it "returns an existing EntityStore instance for URI that exists" do
         | 
| 25 | 
            +
                store = @storage.resolve_entitystore_uri('heap:/')
         | 
| 26 | 
            +
                @storage.resolve_entitystore_uri('heap:/').should.be store
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
              it "clears all URI -> store mappings with #clear" do
         | 
| 29 | 
            +
                meta = @storage.resolve_metastore_uri('heap:/')
         | 
| 30 | 
            +
                entity = @storage.resolve_entitystore_uri('heap:/')
         | 
| 31 | 
            +
                @storage.clear
         | 
| 32 | 
            +
                @storage.resolve_metastore_uri('heap:/').should.not.be meta
         | 
| 33 | 
            +
                @storage.resolve_entitystore_uri('heap:/').should.not.be entity
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
              describe 'Heap Store URIs' do
         | 
| 37 | 
            +
                %w[heap:/ mem:/].each do |uri|
         | 
| 38 | 
            +
                  it "resolves #{uri} meta store URIs" do
         | 
| 39 | 
            +
                    @storage.resolve_metastore_uri(uri).
         | 
| 40 | 
            +
                      should.be.kind_of Rack::Cache::MetaStore
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                  it "resolves #{uri} entity store URIs" do
         | 
| 43 | 
            +
                    @storage.resolve_entitystore_uri(uri).
         | 
| 44 | 
            +
                      should.be.kind_of Rack::Cache::EntityStore
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
              describe 'Disk Store URIs' do
         | 
| 50 | 
            +
                before do
         | 
| 51 | 
            +
                  @temp_dir = create_temp_directory
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
                after do
         | 
| 54 | 
            +
                  remove_entry_secure @temp_dir
         | 
| 55 | 
            +
                  @temp_dir = nil
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                %w[file: disk:].each do |uri|
         | 
| 59 | 
            +
                  it "resolves #{uri} meta store URIs" do
         | 
| 60 | 
            +
                    @storage.resolve_metastore_uri(uri + @temp_dir).
         | 
| 61 | 
            +
                      should.be.kind_of Rack::Cache::MetaStore
         | 
| 62 | 
            +
                  end
         | 
| 63 | 
            +
                  it "resolves #{uri} entity store URIs" do
         | 
| 64 | 
            +
                    @storage.resolve_entitystore_uri(uri + @temp_dir).
         | 
| 65 | 
            +
                      should.be.kind_of Rack::Cache::EntityStore
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
              end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
              if have_memcached?
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                describe 'MemCache Store URIs' do
         | 
| 73 | 
            +
                  %w[memcache: memcached:].each do |scheme|
         | 
| 74 | 
            +
                    it "resolves #{scheme} meta store URIs" do
         | 
| 75 | 
            +
                      uri = scheme + '//' + ENV['MEMCACHED']
         | 
| 76 | 
            +
                      @storage.resolve_metastore_uri(uri).
         | 
| 77 | 
            +
                        should.be.kind_of Rack::Cache::MetaStore
         | 
| 78 | 
            +
                    end
         | 
| 79 | 
            +
                    it "resolves #{scheme} entity store URIs" do
         | 
| 80 | 
            +
                      uri = scheme + '//' + ENV['MEMCACHED']
         | 
| 81 | 
            +
                      @storage.resolve_entitystore_uri(uri).
         | 
| 82 | 
            +
                        should.be.kind_of Rack::Cache::EntityStore
         | 
| 83 | 
            +
                    end
         | 
| 84 | 
            +
                  end
         | 
| 85 | 
            +
                  it 'supports namespaces in memcached: URIs' do
         | 
| 86 | 
            +
                    uri = "memcached://" + ENV['MEMCACHED'] + "/namespace"
         | 
| 87 | 
            +
                    @storage.resolve_metastore_uri(uri).
         | 
| 88 | 
            +
                       should.be.kind_of Rack::Cache::MetaStore
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
              end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
            end
         |