rails-cache-tags 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .idea
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rails-cache-tags.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 amikhailov
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Synopsys
2
+
3
+ Tagged caching support within your Rails application.
4
+
5
+ # Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'rails-cache-tags'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install rails-cache-tags
18
+
19
+ # Usage
20
+
21
+ Anywhere:
22
+
23
+ ```ruby
24
+ cache = Rails.cache
25
+
26
+ cache.write "foo", "bar", :tags => %w(baz foo)
27
+ cache.read "foo" # => "bar"
28
+
29
+ cache.delete_tag "baz"
30
+ cache.read "foo" => nil
31
+ ```
32
+
33
+ In your controller:
34
+ ```ruby
35
+ class PostController < ActionController::Base
36
+ def update
37
+ @post = Post.find_by_id(params[:id])
38
+
39
+ if @post.update_attributes(params)
40
+ expire_fragments_by_tags :post => @post.id
41
+ else
42
+ render :edit
43
+ end
44
+ end
45
+ end
46
+ ```
47
+
48
+ # Contributing
49
+
50
+ 1. Fork it
51
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
52
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
53
+ 4. Push to the branch (`git push origin my-new-feature`)
54
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ desc 'Run tests'
5
+ task :test do
6
+ test_dir = File.expand_path(File.join("..", "test"), __FILE__)
7
+
8
+ Dir[File.join(test_dir, '**', '*_test.rb')].each { |f| require f }
9
+ require 'minitest/autorun'
10
+ end
@@ -0,0 +1,27 @@
1
+ require "active_support/cache"
2
+
3
+ require "rails/cache/tag"
4
+ require "rails/cache/tags/store"
5
+
6
+ module ActiveSupport
7
+ module Cache
8
+ class Store
9
+ extend Rails::Cache::Tags::Store
10
+ end
11
+
12
+ class Entry
13
+ attr_accessor :tags
14
+ end
15
+ end
16
+ end
17
+
18
+ if defined?(ActionController::Base)
19
+ class ActionController::Base < ActionController::Metal
20
+ def expire_fragments_by_tags *args
21
+ return unless cache_configured?
22
+
23
+ cache_store.delete_tag *args
24
+ end
25
+ alias expire_fragments_by_tag expire_fragments_by_tags
26
+ end
27
+ end
@@ -0,0 +1,60 @@
1
+ module Rails
2
+ module Cache
3
+ class Tag #:nodoc:all:
4
+ KEY_PREFIX = '_tags'
5
+
6
+ class << self
7
+ # {:post => ['1', '2', '3']} => [Tag(post:1), Tag(post:2), Tag(post:3)]
8
+ # {:post => 1, :user => 2} => [Tag(post:1), Tag(user:2)]
9
+ # ['post:1', 'post:2', 'post:3'] => [Tag(post:1), Tag(post:2), Tag(post:3)]
10
+ def build_tags(names)
11
+ case names
12
+ when NilClass then nil
13
+ when Hash then names.map do |key, value|
14
+ Array.wrap(value).map { |v| new([key, v]) }
15
+ end.flatten
16
+ when Enumerable then names.map { |v| build_tags(v) }.flatten
17
+ when self then names
18
+ else [new(names)]
19
+ end
20
+ end
21
+ end
22
+
23
+ attr_reader :name
24
+
25
+ # Tag constructor, accepts String, Symbol and Array
26
+ def initialize(name)
27
+ @name = case name
28
+ when String, Symbol then name
29
+ when Array then name.join(':')
30
+ else raise ArgumentError
31
+ end
32
+ end
33
+
34
+ # real cache key
35
+ def to_key
36
+ [KEY_PREFIX, name].join('/')
37
+ end
38
+
39
+ # read tag's version from +store+
40
+ def fetch(store)
41
+ store.read(to_key)
42
+ end
43
+
44
+ # increment tag's version inside +store+
45
+ def increment(store)
46
+ current = fetch(store)
47
+
48
+ version = if current.is_a?(Fixnum)
49
+ current + 1
50
+ else
51
+ 1
52
+ end
53
+
54
+ store.write(to_key, version, :expires_in => nil)
55
+
56
+ version
57
+ end
58
+ end # class Tag
59
+ end # module Cache
60
+ end # module Rails
@@ -0,0 +1,78 @@
1
+ module Rails
2
+ module Cache
3
+ module Tags
4
+ module Store
5
+ # patched +new+ method
6
+ def new(*args, &block) #:nodoc:
7
+ unless acts_like?(:cached_tags)
8
+ extend ClassMethods
9
+ include InstanceMethods
10
+
11
+ alias_method_chain :read_entry, :tags
12
+ alias_method_chain :write_entry, :tags
13
+ end
14
+
15
+ super
16
+ end
17
+
18
+ module ClassMethods #:nodoc:all:
19
+ def acts_like_cached_tags?
20
+ end
21
+ end
22
+
23
+ module InstanceMethods
24
+ # Increment the version of tags, so all entries referring to the tags become invalid
25
+ def delete_tag *names
26
+ tags = Rails::Cache::Tag.build_tags(names)
27
+
28
+ tags.each { |tag| tag.increment(self) } unless tags.empty?
29
+ end
30
+ alias delete_by_tag delete_tag
31
+ alias delete_by_tags delete_tag
32
+ alias expire_tag delete_tag
33
+
34
+ protected
35
+ def read_entry_with_tags(key, options) #:nodoc
36
+ entry = read_entry_without_tags(key, options)
37
+
38
+ if entry && entry.tags.present?
39
+ current_versions = fetch_tags(entry.tags.keys)
40
+ saved_versions = entry.tags.values
41
+
42
+ if current_versions != saved_versions
43
+ delete_entry(key, options)
44
+
45
+ return nil
46
+ end
47
+ end
48
+
49
+ entry
50
+ end # def read_entry_with_tags
51
+
52
+ def write_entry_with_tags(key, entry, options) #:nodoc:
53
+ tags = Rails::Cache::Tag.build_tags Array.wrap(options[:tags]).flatten.compact
54
+
55
+ unless tags.empty?
56
+ current_versions = fetch_tags(tags) # => [1, 2, 3]
57
+ entry.tags = Hash[tags.zip(current_versions).map { |tag, v| [tag.name, v || tag.increment(self)] }]
58
+ end
59
+
60
+ write_entry_without_tags(key, entry, options)
61
+ end # def write_entry_without_tags
62
+
63
+ private
64
+ # fetch tags versions from store
65
+ # fetch ['user:1', 'post:2', 'country:2'] => [3, 4, nil]
66
+ def fetch_tags(names) #:nodoc:
67
+ tags = Rails::Cache::Tag.build_tags names
68
+ keys = tags.map(&:to_key)
69
+ stored = read_multi(*keys)
70
+
71
+ # we should save order
72
+ keys.collect { |k| stored[k] }
73
+ end
74
+ end
75
+ end # module Store
76
+ end # module Tags
77
+ end # module Cache
78
+ end # module Rails
@@ -0,0 +1,7 @@
1
+ module Rails
2
+ module Cache
3
+ module Tags
4
+ VERSION = "1.0.0"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/rails/cache/tags/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Alexei Mikhailov"]
6
+ gem.email = %W(amikhailov83@gmail.com)
7
+ gem.description = %q{Tagged caching support for Rails}
8
+ gem.summary = %q{Tagged caching support for Rails}
9
+ gem.homepage = "https://github.com/take-five/rails-cache-tags"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
13
+ gem.name = "rails-cache-tags"
14
+ gem.require_paths = %W(lib)
15
+ gem.version = Rails::Cache::Tags::VERSION
16
+
17
+ gem.add_dependency "activesupport", ">= 3.0"
18
+ gem.add_dependency "actionpack", ">= 3.0"
19
+
20
+ gem.add_development_dependency 'minitest', '~> 3.2'
21
+ gem.add_development_dependency 'memcache-client'
22
+ gem.add_development_dependency 'rack'
23
+ gem.add_development_dependency 'mocha'
24
+ gem.add_development_dependency 'simplecov'
25
+ end
@@ -0,0 +1,80 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+
3
+ require "test_helper"
4
+ require "caching_test"
5
+
6
+ module CacheTagsBehavior
7
+ def test_read_and_write_with_tags
8
+ @cache.write("foo", "bar", :tags => "baz")
9
+ assert_equal 'bar', @cache.read('foo')
10
+ end
11
+
12
+ def test_read_after_tag_deleted
13
+ @cache.write("foo", "bar", :tags => "baz")
14
+ @cache.delete_tag("baz")
15
+
16
+ assert_nil @cache.read("foo")
17
+ end
18
+
19
+ def test_read_after_another_tag_deleted
20
+ @cache.write("foo", "bar", :tags => "baz")
21
+ @cache.delete_tag("fu")
22
+
23
+ assert_equal 'bar', @cache.read('foo')
24
+ end
25
+
26
+ def test_read_and_write_with_multiple_tags
27
+ @cache.write("foo", "bar", :tags => [:baz, :kung])
28
+ assert_equal 'bar', @cache.read('foo')
29
+ end
30
+
31
+ def test_read_after_one_of_tags_deleted
32
+ @cache.write("foo", "bar", :tags => [:baz, :kung])
33
+ @cache.delete_tag :kung
34
+
35
+ assert_nil @cache.read("foo")
36
+ end
37
+
38
+ def test_read_after_another_of_multiple_tags_deleted
39
+ @cache.write("foo", "bar", :tags => [:baz, :kung])
40
+ @cache.delete_tag("fu")
41
+
42
+ assert_equal 'bar', @cache.read('foo')
43
+ end
44
+
45
+ def test_read_with_small_default_expiration_time
46
+ cache = if is_a?(FileStoreTest)
47
+ @cache.class.new @cache.cache_path, :expires_in => 0.001
48
+ else
49
+ @cache.class.new :expires_in => 0.001
50
+ end
51
+
52
+ cache.write("foo", "bar", :tags => "baz", :expires_in => 2)
53
+ sleep 0.02
54
+
55
+ assert_equal 'bar', cache.read('foo')
56
+ end
57
+
58
+ def test_exists_with_tags
59
+ @cache.write("foo", "bar", :tags => "baz")
60
+ @cache.delete_tag("baz")
61
+
62
+ assert_equal @cache.exist?("foo"), false
63
+ end
64
+
65
+ def test_read_and_write_with_tags_hash
66
+ @cache.write("foo", "bar", :tags => {:baz => 1})
67
+ assert_equal 'bar', @cache.read('foo')
68
+ end
69
+
70
+ def test_read_and_write_with_tags_hash_after_expiration
71
+ @cache.write("foo", "bar", :tags => {:baz => 1})
72
+ @cache.delete_tag :baz => 1
73
+
74
+ assert_nil @cache.read('foo')
75
+ end
76
+ end
77
+
78
+ [FileStoreTest, MemoryStoreTest, MemCacheStoreTest].each do |klass|
79
+ klass.send :include, CacheTagsBehavior
80
+ end
@@ -0,0 +1,846 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+
3
+ require 'test_helper'
4
+ require 'logger'
5
+
6
+ class CacheKeyTest < ActiveSupport::TestCase
7
+ def test_expand_cache_key
8
+ assert_equal '1/2/true', ActiveSupport::Cache.expand_cache_key([1, '2', true])
9
+ assert_equal 'name/1/2/true', ActiveSupport::Cache.expand_cache_key([1, '2', true], :name)
10
+ end
11
+
12
+ def test_expand_cache_key_with_rails_cache_id
13
+ begin
14
+ ENV['RAILS_CACHE_ID'] = 'c99'
15
+ assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key(:foo)
16
+ assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key([:foo])
17
+ assert_equal 'c99/foo/bar', ActiveSupport::Cache.expand_cache_key([:foo, :bar])
18
+ assert_equal 'nm/c99/foo', ActiveSupport::Cache.expand_cache_key(:foo, :nm)
19
+ assert_equal 'nm/c99/foo', ActiveSupport::Cache.expand_cache_key([:foo], :nm)
20
+ assert_equal 'nm/c99/foo/bar', ActiveSupport::Cache.expand_cache_key([:foo, :bar], :nm)
21
+ ensure
22
+ ENV['RAILS_CACHE_ID'] = nil
23
+ end
24
+ end
25
+
26
+ def test_expand_cache_key_with_rails_app_version
27
+ begin
28
+ ENV['RAILS_APP_VERSION'] = 'rails3'
29
+ assert_equal 'rails3/foo', ActiveSupport::Cache.expand_cache_key(:foo)
30
+ ensure
31
+ ENV['RAILS_APP_VERSION'] = nil
32
+ end
33
+ end
34
+
35
+ def test_expand_cache_key_rails_cache_id_should_win_over_rails_app_version
36
+ begin
37
+ ENV['RAILS_CACHE_ID'] = 'c99'
38
+ ENV['RAILS_APP_VERSION'] = 'rails3'
39
+ assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key(:foo)
40
+ ensure
41
+ ENV['RAILS_CACHE_ID'] = nil
42
+ ENV['RAILS_APP_VERSION'] = nil
43
+ end
44
+ end
45
+
46
+ def test_expand_cache_key_respond_to_cache_key
47
+ key = 'foo'
48
+ def key.cache_key
49
+ :foo_key
50
+ end
51
+ assert_equal 'foo_key', ActiveSupport::Cache.expand_cache_key(key)
52
+ end
53
+
54
+ def test_expand_cache_key_array_with_something_that_responds_to_cache_key
55
+ key = 'foo'
56
+ def key.cache_key
57
+ :foo_key
58
+ end
59
+ assert_equal 'foo_key', ActiveSupport::Cache.expand_cache_key([key])
60
+ end
61
+
62
+ def test_expand_cache_key_of_nil
63
+ assert_equal '', ActiveSupport::Cache.expand_cache_key(nil)
64
+ end
65
+
66
+ def test_expand_cache_key_of_false
67
+ assert_equal 'false', ActiveSupport::Cache.expand_cache_key(false)
68
+ end
69
+
70
+ def test_expand_cache_key_of_true
71
+ assert_equal 'true', ActiveSupport::Cache.expand_cache_key(true)
72
+ end
73
+ end
74
+
75
+ class CacheStoreSettingTest < ActiveSupport::TestCase
76
+ def test_file_fragment_cache_store
77
+ store = ActiveSupport::Cache.lookup_store :file_store, "/path/to/cache/directory"
78
+ assert_kind_of(ActiveSupport::Cache::FileStore, store)
79
+ assert_equal "/path/to/cache/directory", store.cache_path
80
+ end
81
+
82
+ def test_mem_cache_fragment_cache_store
83
+ MemCache.expects(:new).with(%w[localhost], {})
84
+ store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost"
85
+ assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
86
+ end
87
+
88
+ def test_mem_cache_fragment_cache_store_with_given_mem_cache
89
+ mem_cache = MemCache.new
90
+ MemCache.expects(:new).never
91
+ store = ActiveSupport::Cache.lookup_store :mem_cache_store, mem_cache
92
+ assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
93
+ end
94
+
95
+ def test_mem_cache_fragment_cache_store_with_given_mem_cache_like_object
96
+ MemCache.expects(:new).never
97
+ memcache = Object.new
98
+ def memcache.get() true end
99
+ store = ActiveSupport::Cache.lookup_store :mem_cache_store, memcache
100
+ assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
101
+ end
102
+
103
+ def test_mem_cache_fragment_cache_store_with_multiple_servers
104
+ MemCache.expects(:new).with(%w[localhost 192.168.1.1], {})
105
+ store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1'
106
+ assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
107
+ end
108
+
109
+ def test_mem_cache_fragment_cache_store_with_options
110
+ MemCache.expects(:new).with(%w[localhost 192.168.1.1], { :timeout => 10 })
111
+ store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost", '192.168.1.1', :namespace => 'foo', :timeout => 10
112
+ assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
113
+ assert_equal 'foo', store.options[:namespace]
114
+ end
115
+
116
+ def test_object_assigned_fragment_cache_store
117
+ store = ActiveSupport::Cache.lookup_store ActiveSupport::Cache::FileStore.new("/path/to/cache/directory")
118
+ assert_kind_of(ActiveSupport::Cache::FileStore, store)
119
+ assert_equal "/path/to/cache/directory", store.cache_path
120
+ end
121
+ end
122
+
123
+ class CacheStoreNamespaceTest < ActiveSupport::TestCase
124
+ def test_static_namespace
125
+ cache = ActiveSupport::Cache.lookup_store(:memory_store, :namespace => "tester")
126
+ cache.write("foo", "bar")
127
+ assert_equal "bar", cache.read("foo")
128
+ assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value
129
+ end
130
+
131
+ def test_proc_namespace
132
+ test_val = "tester"
133
+ proc = lambda{test_val}
134
+ cache = ActiveSupport::Cache.lookup_store(:memory_store, :namespace => proc)
135
+ cache.write("foo", "bar")
136
+ assert_equal "bar", cache.read("foo")
137
+ assert_equal "bar", cache.instance_variable_get(:@data)["tester:foo"].value
138
+ end
139
+
140
+ def test_delete_matched_key_start
141
+ cache = ActiveSupport::Cache.lookup_store(:memory_store, :namespace => "tester")
142
+ cache.write("foo", "bar")
143
+ cache.write("fu", "baz")
144
+ cache.delete_matched(/^fo/)
145
+ assert !cache.exist?("foo")
146
+ assert cache.exist?("fu")
147
+ end
148
+
149
+ def test_delete_matched_key
150
+ cache = ActiveSupport::Cache.lookup_store(:memory_store, :namespace => "foo")
151
+ cache.write("foo", "bar")
152
+ cache.write("fu", "baz")
153
+ cache.delete_matched(/OO/i)
154
+ assert !cache.exist?("foo")
155
+ assert cache.exist?("fu")
156
+ end
157
+ end
158
+
159
+ # Tests the base functionality that should be identical across all cache stores.
160
+ module CacheStoreBehavior
161
+ def test_should_read_and_write_strings
162
+ assert @cache.write('foo', 'bar')
163
+ assert_equal 'bar', @cache.read('foo')
164
+ end
165
+
166
+ def test_should_overwrite
167
+ @cache.write('foo', 'bar')
168
+ @cache.write('foo', 'baz')
169
+ assert_equal 'baz', @cache.read('foo')
170
+ end
171
+
172
+ def test_fetch_without_cache_miss
173
+ @cache.write('foo', 'bar')
174
+ @cache.expects(:write).never
175
+ assert_equal 'bar', @cache.fetch('foo') { 'baz' }
176
+ end
177
+
178
+ def test_fetch_with_cache_miss
179
+ @cache.expects(:write).with('foo', 'baz', @cache.options)
180
+ assert_equal 'baz', @cache.fetch('foo') { 'baz' }
181
+ end
182
+
183
+ def test_fetch_with_forced_cache_miss
184
+ @cache.write('foo', 'bar')
185
+ @cache.expects(:read).never
186
+ @cache.expects(:write).with('foo', 'bar', @cache.options.merge(:force => true))
187
+ @cache.fetch('foo', :force => true) { 'bar' }
188
+ end
189
+
190
+ def test_fetch_with_cached_nil
191
+ @cache.write('foo', nil)
192
+ @cache.expects(:write).never
193
+ assert_nil @cache.fetch('foo') { 'baz' }
194
+ end
195
+
196
+ def test_should_read_and_write_hash
197
+ assert @cache.write('foo', {:a => "b"})
198
+ assert_equal({:a => "b"}, @cache.read('foo'))
199
+ end
200
+
201
+ def test_should_read_and_write_integer
202
+ assert @cache.write('foo', 1)
203
+ assert_equal 1, @cache.read('foo')
204
+ end
205
+
206
+ def test_should_read_and_write_nil
207
+ assert @cache.write('foo', nil)
208
+ assert_equal nil, @cache.read('foo')
209
+ end
210
+
211
+ def test_should_read_and_write_false
212
+ assert @cache.write('foo', false)
213
+ assert_equal false, @cache.read('foo')
214
+ end
215
+
216
+ def test_should_read_cached_numeric_from_previous_rails_versions
217
+ @old_cache = ActiveSupport::Cache::Entry.create( 1, Time.now )
218
+ assert_equal( 1, @old_cache.value )
219
+ end
220
+
221
+ def test_should_read_cached_hash_from_previous_rails_versions
222
+ @old_cache = ActiveSupport::Cache::Entry.create( {}, Time.now )
223
+ assert_equal( {}, @old_cache.value )
224
+ end
225
+
226
+ def test_should_read_cached_string_from_previous_rails_versions
227
+ @old_cache = ActiveSupport::Cache::Entry.create( 'string', Time.now )
228
+ assert_equal( 'string', @old_cache.value )
229
+ end
230
+
231
+ def test_read_multi
232
+ @cache.write('foo', 'bar')
233
+ @cache.write('fu', 'baz')
234
+ @cache.write('fud', 'biz')
235
+ assert_equal({"foo" => "bar", "fu" => "baz"}, @cache.read_multi('foo', 'fu'))
236
+ end
237
+
238
+ def test_read_multi_with_expires
239
+ @cache.write('foo', 'bar', :expires_in => 0.001)
240
+ @cache.write('fu', 'baz')
241
+ @cache.write('fud', 'biz')
242
+ sleep(0.002)
243
+ assert_equal({"fu" => "baz"}, @cache.read_multi('foo', 'fu'))
244
+ end
245
+
246
+ def test_read_and_write_compressed_small_data
247
+ @cache.write('foo', 'bar', :compress => true)
248
+ raw_value = @cache.send(:read_entry, 'foo', {}).raw_value
249
+ assert_equal 'bar', @cache.read('foo')
250
+ assert_equal 'bar', Marshal.load(raw_value)
251
+ end
252
+
253
+ def test_read_and_write_compressed_large_data
254
+ @cache.write('foo', 'bar', :compress => true, :compress_threshold => 2)
255
+ raw_value = @cache.send(:read_entry, 'foo', {}).raw_value
256
+ assert_equal 'bar', @cache.read('foo')
257
+ assert_equal 'bar', Marshal.load(Zlib::Inflate.inflate(raw_value))
258
+ end
259
+
260
+ def test_read_and_write_compressed_nil
261
+ @cache.write('foo', nil, :compress => true)
262
+ assert_nil @cache.read('foo')
263
+ end
264
+
265
+ def test_cache_key
266
+ obj = Object.new
267
+ def obj.cache_key
268
+ :foo
269
+ end
270
+ @cache.write(obj, "bar")
271
+ assert_equal "bar", @cache.read("foo")
272
+ end
273
+
274
+ def test_param_as_cache_key
275
+ obj = Object.new
276
+ def obj.to_param
277
+ "foo"
278
+ end
279
+ @cache.write(obj, "bar")
280
+ assert_equal "bar", @cache.read("foo")
281
+ end
282
+
283
+ def test_array_as_cache_key
284
+ @cache.write([:fu, "foo"], "bar")
285
+ assert_equal "bar", @cache.read("fu/foo")
286
+ end
287
+
288
+ def test_hash_as_cache_key
289
+ @cache.write({:foo => 1, :fu => 2}, "bar")
290
+ assert_equal "bar", @cache.read("foo=1/fu=2")
291
+ end
292
+
293
+ def test_keys_are_case_sensitive
294
+ @cache.write("foo", "bar")
295
+ assert_nil @cache.read("FOO")
296
+ end
297
+
298
+ def test_exist
299
+ @cache.write('foo', 'bar')
300
+ assert @cache.exist?('foo')
301
+ assert !@cache.exist?('bar')
302
+ end
303
+
304
+ def test_nil_exist
305
+ @cache.write('foo', nil)
306
+ assert @cache.exist?('foo')
307
+ end
308
+
309
+ def test_delete
310
+ @cache.write('foo', 'bar')
311
+ assert @cache.exist?('foo')
312
+ assert @cache.delete('foo')
313
+ assert !@cache.exist?('foo')
314
+ end
315
+
316
+ def test_read_should_return_a_different_object_id_each_time_it_is_called
317
+ @cache.write('foo', 'bar')
318
+ assert_not_equal @cache.read('foo').object_id, @cache.read('foo').object_id
319
+ value = @cache.read('foo')
320
+ value << 'bingo'
321
+ assert_not_equal value, @cache.read('foo')
322
+ end
323
+
324
+ def test_original_store_objects_should_not_be_immutable
325
+ bar = 'bar'
326
+ @cache.write('foo', bar)
327
+ assert_nothing_raised { bar.gsub!(/.*/, 'baz') }
328
+ end
329
+
330
+ def test_expires_in
331
+ time = Time.local(2008, 4, 24)
332
+ Time.stubs(:now).returns(time)
333
+
334
+ @cache.write('foo', 'bar')
335
+ assert_equal 'bar', @cache.read('foo')
336
+
337
+ Time.stubs(:now).returns(time + 30)
338
+ assert_equal 'bar', @cache.read('foo')
339
+
340
+ Time.stubs(:now).returns(time + 61)
341
+ assert_nil @cache.read('foo')
342
+ end
343
+
344
+ def test_race_condition_protection
345
+ time = Time.now
346
+ @cache.write('foo', 'bar', :expires_in => 60)
347
+ Time.stubs(:now).returns(time + 61)
348
+ result = @cache.fetch('foo', :race_condition_ttl => 10) do
349
+ assert_equal 'bar', @cache.read('foo')
350
+ "baz"
351
+ end
352
+ assert_equal "baz", result
353
+ end
354
+
355
+ def test_race_condition_protection_is_limited
356
+ time = Time.now
357
+ @cache.write('foo', 'bar', :expires_in => 60)
358
+ Time.stubs(:now).returns(time + 71)
359
+ result = @cache.fetch('foo', :race_condition_ttl => 10) do
360
+ assert_equal nil, @cache.read('foo')
361
+ "baz"
362
+ end
363
+ assert_equal "baz", result
364
+ end
365
+
366
+ def test_race_condition_protection_is_safe
367
+ time = Time.now
368
+ @cache.write('foo', 'bar', :expires_in => 60)
369
+ Time.stubs(:now).returns(time + 61)
370
+ begin
371
+ @cache.fetch('foo', :race_condition_ttl => 10) do
372
+ assert_equal 'bar', @cache.read('foo')
373
+ raise ArgumentError.new
374
+ end
375
+ rescue ArgumentError
376
+ end
377
+ assert_equal "bar", @cache.read('foo')
378
+ Time.stubs(:now).returns(time + 71)
379
+ assert_nil @cache.read('foo')
380
+ end
381
+
382
+ def test_crazy_key_characters
383
+ crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-"
384
+ assert @cache.write(crazy_key, "1", :raw => true)
385
+ assert_equal "1", @cache.read(crazy_key)
386
+ assert_equal "1", @cache.fetch(crazy_key)
387
+ assert @cache.delete(crazy_key)
388
+ assert_equal "2", @cache.fetch(crazy_key, :raw => true) { "2" }
389
+ assert_equal 3, @cache.increment(crazy_key)
390
+ assert_equal 2, @cache.decrement(crazy_key)
391
+ end
392
+
393
+ def test_really_long_keys
394
+ key = ""
395
+ 900.times{key << "x"}
396
+ assert @cache.write(key, "bar")
397
+ assert_equal "bar", @cache.read(key)
398
+ assert_equal "bar", @cache.fetch(key)
399
+ assert_nil @cache.read("#{key}x")
400
+ assert_equal({key => "bar"}, @cache.read_multi(key))
401
+ assert @cache.delete(key)
402
+ end
403
+ end
404
+
405
+ # https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters
406
+ # The error is caused by charcter encodings that can't be compared with ASCII-8BIT regular expressions and by special
407
+ # characters like the umlaut in UTF-8.
408
+ module EncodedKeyCacheBehavior
409
+ if defined?(Encoding)
410
+ Encoding.list.each do |encoding|
411
+ define_method "test_#{encoding.name.underscore}_encoded_values" do
412
+ key = "foo".force_encoding(encoding)
413
+ assert @cache.write(key, "1", :raw => true)
414
+ assert_equal "1", @cache.read(key)
415
+ assert_equal "1", @cache.fetch(key)
416
+ assert @cache.delete(key)
417
+ assert_equal "2", @cache.fetch(key, :raw => true) { "2" }
418
+ assert_equal 3, @cache.increment(key)
419
+ assert_equal 2, @cache.decrement(key)
420
+ end
421
+ end
422
+
423
+ def test_common_utf8_values
424
+ key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8)
425
+ assert @cache.write(key, "1", :raw => true)
426
+ assert_equal "1", @cache.read(key)
427
+ assert_equal "1", @cache.fetch(key)
428
+ assert @cache.delete(key)
429
+ assert_equal "2", @cache.fetch(key, :raw => true) { "2" }
430
+ assert_equal 3, @cache.increment(key)
431
+ assert_equal 2, @cache.decrement(key)
432
+ end
433
+
434
+ def test_retains_encoding
435
+ key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8)
436
+ assert @cache.write(key, "1", :raw => true)
437
+ assert_equal Encoding::UTF_8, key.encoding
438
+ end
439
+ end
440
+ end
441
+
442
+ module CacheDeleteMatchedBehavior
443
+ def test_delete_matched
444
+ @cache.write("foo", "bar")
445
+ @cache.write("fu", "baz")
446
+ @cache.write("foo/bar", "baz")
447
+ @cache.write("fu/baz", "bar")
448
+ @cache.delete_matched(/oo/)
449
+ assert !@cache.exist?("foo")
450
+ assert @cache.exist?("fu")
451
+ assert !@cache.exist?("foo/bar")
452
+ assert @cache.exist?("fu/baz")
453
+ end
454
+ end
455
+
456
+ module CacheIncrementDecrementBehavior
457
+ def test_increment
458
+ @cache.write('foo', 1, :raw => true)
459
+ assert_equal 1, @cache.read('foo').to_i
460
+ assert_equal 2, @cache.increment('foo')
461
+ assert_equal 2, @cache.read('foo').to_i
462
+ assert_equal 3, @cache.increment('foo')
463
+ assert_equal 3, @cache.read('foo').to_i
464
+ end
465
+
466
+ def test_decrement
467
+ @cache.write('foo', 3, :raw => true)
468
+ assert_equal 3, @cache.read('foo').to_i
469
+ assert_equal 2, @cache.decrement('foo')
470
+ assert_equal 2, @cache.read('foo').to_i
471
+ assert_equal 1, @cache.decrement('foo')
472
+ assert_equal 1, @cache.read('foo').to_i
473
+ end
474
+ end
475
+
476
+ module LocalCacheBehavior
477
+ def test_local_writes_are_persistent_on_the_remote_cache
478
+ retval = @cache.with_local_cache do
479
+ @cache.write('foo', 'bar')
480
+ end
481
+ assert retval
482
+ assert_equal 'bar', @cache.read('foo')
483
+ end
484
+
485
+ def test_clear_also_clears_local_cache
486
+ @cache.with_local_cache do
487
+ @cache.write('foo', 'bar')
488
+ @cache.clear
489
+ assert_nil @cache.read('foo')
490
+ end
491
+
492
+ assert_nil @cache.read('foo')
493
+ end
494
+
495
+ def test_local_cache_of_write
496
+ @cache.with_local_cache do
497
+ @cache.write('foo', 'bar')
498
+ @peek.delete('foo')
499
+ assert_equal 'bar', @cache.read('foo')
500
+ end
501
+ end
502
+
503
+ def test_local_cache_of_read
504
+ @cache.write('foo', 'bar')
505
+ @cache.with_local_cache do
506
+ assert_equal 'bar', @cache.read('foo')
507
+ end
508
+ end
509
+
510
+ def test_local_cache_of_write_nil
511
+ @cache.with_local_cache do
512
+ assert @cache.write('foo', nil)
513
+ assert_nil @cache.read('foo')
514
+ @peek.write('foo', 'bar')
515
+ assert_nil @cache.read('foo')
516
+ end
517
+ end
518
+
519
+ def test_local_cache_of_delete
520
+ @cache.with_local_cache do
521
+ @cache.write('foo', 'bar')
522
+ @cache.delete('foo')
523
+ assert_nil @cache.read('foo')
524
+ end
525
+ end
526
+
527
+ def test_local_cache_of_exist
528
+ @cache.with_local_cache do
529
+ @cache.write('foo', 'bar')
530
+ @peek.delete('foo')
531
+ assert @cache.exist?('foo')
532
+ end
533
+ end
534
+
535
+ def test_local_cache_of_increment
536
+ @cache.with_local_cache do
537
+ @cache.write('foo', 1, :raw => true)
538
+ @peek.write('foo', 2, :raw => true)
539
+ @cache.increment('foo')
540
+ assert_equal 3, @cache.read('foo')
541
+ end
542
+ end
543
+
544
+ def test_local_cache_of_decrement
545
+ @cache.with_local_cache do
546
+ @cache.write('foo', 1, :raw => true)
547
+ @peek.write('foo', 3, :raw => true)
548
+ @cache.decrement('foo')
549
+ assert_equal 2, @cache.read('foo')
550
+ end
551
+ end
552
+
553
+ def test_middleware
554
+ app = lambda { |env|
555
+ result = @cache.write('foo', 'bar')
556
+ assert_equal 'bar', @cache.read('foo') # make sure 'foo' was written
557
+ assert result
558
+ }
559
+ app = @cache.middleware.new(app)
560
+ app.call({})
561
+ end
562
+ end
563
+
564
+ class FileStoreTest < ActiveSupport::TestCase
565
+ def setup
566
+ Dir.mkdir(cache_dir) unless File.exist?(cache_dir)
567
+ @cache = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, :expires_in => 60)
568
+ @peek = ActiveSupport::Cache.lookup_store(:file_store, cache_dir, :expires_in => 60)
569
+ @cache_with_pathname = ActiveSupport::Cache.lookup_store(:file_store, Pathname.new(cache_dir), :expires_in => 60)
570
+ end
571
+
572
+ def teardown
573
+ FileUtils.rm_r(cache_dir)
574
+ end
575
+
576
+ def cache_dir
577
+ File.join(Dir.pwd, 'tmp_cache')
578
+ end
579
+
580
+ include CacheStoreBehavior
581
+ include LocalCacheBehavior
582
+ include CacheDeleteMatchedBehavior
583
+ include CacheIncrementDecrementBehavior
584
+
585
+ def test_key_transformation
586
+ key = @cache.send(:key_file_path, "views/index?id=1")
587
+ assert_equal "views/index?id=1", @cache.send(:file_path_key, key)
588
+ end
589
+
590
+ def test_key_transformation_with_pathname
591
+ FileUtils.touch(File.join(cache_dir, "foo"))
592
+ key = @cache_with_pathname.send(:key_file_path, "views/index?id=1")
593
+ assert_equal "views/index?id=1", @cache_with_pathname.send(:file_path_key, key)
594
+ end
595
+
596
+ # Because file systems have a maximum filename size, filenames > max size should be split in to directories
597
+ # If filename is 'AAAAB', where max size is 4, the returned path should be AAAA/B
598
+ def test_key_transformation_max_filename_size
599
+ key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}B"
600
+ path = @cache.send(:key_file_path, key)
601
+ assert path.split('/').all? { |dir_name| dir_name.size <= ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}
602
+ assert_equal 'B', File.basename(path)
603
+ end
604
+
605
+ # If nothing has been stored in the cache, there is a chance the cache directory does not yet exist
606
+ # Ensure delete_matched gracefully handles this case
607
+ def test_delete_matched_when_cache_directory_does_not_exist
608
+ assert_nothing_raised(Exception) do
609
+ ActiveSupport::Cache::FileStore.new('/test/cache/directory').delete_matched(/does_not_exist/)
610
+ end
611
+ end
612
+ end
613
+
614
+ class MemoryStoreTest < ActiveSupport::TestCase
615
+ def setup
616
+ @record_size = Marshal.dump("aaaaaaaaaa").bytesize
617
+ @cache = ActiveSupport::Cache.lookup_store(:memory_store, :expires_in => 60, :size => @record_size * 10)
618
+ end
619
+
620
+ include CacheStoreBehavior
621
+ include CacheDeleteMatchedBehavior
622
+ include CacheIncrementDecrementBehavior
623
+
624
+ def test_prune_size
625
+ @cache.write(1, "aaaaaaaaaa") && sleep(0.001)
626
+ @cache.write(2, "bbbbbbbbbb") && sleep(0.001)
627
+ @cache.write(3, "cccccccccc") && sleep(0.001)
628
+ @cache.write(4, "dddddddddd") && sleep(0.001)
629
+ @cache.write(5, "eeeeeeeeee") && sleep(0.001)
630
+ @cache.read(2) && sleep(0.001)
631
+ @cache.read(4)
632
+ @cache.prune(@record_size * 3)
633
+ assert @cache.exist?(5)
634
+ assert @cache.exist?(4)
635
+ assert !@cache.exist?(3)
636
+ assert @cache.exist?(2)
637
+ assert !@cache.exist?(1)
638
+ end
639
+
640
+ def test_prune_size_on_write
641
+ @cache.write(1, "aaaaaaaaaa") && sleep(0.001)
642
+ @cache.write(2, "bbbbbbbbbb") && sleep(0.001)
643
+ @cache.write(3, "cccccccccc") && sleep(0.001)
644
+ @cache.write(4, "dddddddddd") && sleep(0.001)
645
+ @cache.write(5, "eeeeeeeeee") && sleep(0.001)
646
+ @cache.write(6, "ffffffffff") && sleep(0.001)
647
+ @cache.write(7, "gggggggggg") && sleep(0.001)
648
+ @cache.write(8, "hhhhhhhhhh") && sleep(0.001)
649
+ @cache.write(9, "iiiiiiiiii") && sleep(0.001)
650
+ @cache.write(10, "kkkkkkkkkk") && sleep(0.001)
651
+ @cache.read(2) && sleep(0.001)
652
+ @cache.read(4) && sleep(0.001)
653
+ @cache.write(11, "llllllllll")
654
+ assert @cache.exist?(11)
655
+ assert @cache.exist?(10)
656
+ assert @cache.exist?(9)
657
+ assert @cache.exist?(8)
658
+ assert @cache.exist?(7)
659
+ assert !@cache.exist?(6)
660
+ assert !@cache.exist?(5)
661
+ assert @cache.exist?(4)
662
+ assert !@cache.exist?(3)
663
+ assert @cache.exist?(2)
664
+ assert !@cache.exist?(1)
665
+ end
666
+
667
+ def test_pruning_is_capped_at_a_max_time
668
+ def @cache.delete_entry (*args)
669
+ sleep(0.01)
670
+ super
671
+ end
672
+ @cache.write(1, "aaaaaaaaaa") && sleep(0.001)
673
+ @cache.write(2, "bbbbbbbbbb") && sleep(0.001)
674
+ @cache.write(3, "cccccccccc") && sleep(0.001)
675
+ @cache.write(4, "dddddddddd") && sleep(0.001)
676
+ @cache.write(5, "eeeeeeeeee") && sleep(0.001)
677
+ @cache.prune(30, 0.001)
678
+ assert @cache.exist?(5)
679
+ assert @cache.exist?(4)
680
+ assert @cache.exist?(3)
681
+ assert @cache.exist?(2)
682
+ assert !@cache.exist?(1)
683
+ end
684
+ end
685
+
686
+ uses_memcached 'memcached backed store' do
687
+ class MemCacheStoreTest < ActiveSupport::TestCase
688
+ def setup
689
+ @cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :expires_in => 60)
690
+ @peek = ActiveSupport::Cache.lookup_store(:mem_cache_store)
691
+ @data = @cache.instance_variable_get(:@data)
692
+ @cache.clear
693
+ @cache.silence!
694
+ @cache.logger = Logger.new("/dev/null")
695
+ end
696
+
697
+ include CacheStoreBehavior
698
+ include LocalCacheBehavior
699
+ include CacheIncrementDecrementBehavior
700
+ include EncodedKeyCacheBehavior
701
+
702
+ def test_raw_values
703
+ cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :raw => true)
704
+ cache.clear
705
+ cache.write("foo", 2)
706
+ assert_equal "2", cache.read("foo")
707
+ end
708
+
709
+ def test_raw_values_with_marshal
710
+ cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :raw => true)
711
+ cache.clear
712
+ cache.write("foo", Marshal.dump([]))
713
+ assert_equal [], cache.read("foo")
714
+ end
715
+
716
+ def test_local_cache_raw_values
717
+ cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :raw => true)
718
+ cache.clear
719
+ cache.with_local_cache do
720
+ cache.write("foo", 2)
721
+ assert_equal "2", cache.read("foo")
722
+ end
723
+ end
724
+
725
+ def test_local_cache_raw_values_with_marshal
726
+ cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :raw => true)
727
+ cache.clear
728
+ cache.with_local_cache do
729
+ cache.write("foo", Marshal.dump([]))
730
+ assert_equal [], cache.read("foo")
731
+ end
732
+ end
733
+ end
734
+ end
735
+
736
+ class NullStoreTest < ActiveSupport::TestCase
737
+ def setup
738
+ @cache = ActiveSupport::Cache.lookup_store(:null_store)
739
+ end
740
+
741
+ def test_clear
742
+ @cache.clear
743
+ end
744
+
745
+ def test_cleanup
746
+ @cache.cleanup
747
+ end
748
+
749
+ def test_write
750
+ assert_equal true, @cache.write("name", "value")
751
+ end
752
+
753
+ def test_read
754
+ @cache.write("name", "value")
755
+ assert_nil @cache.read("name")
756
+ end
757
+
758
+ def test_delete
759
+ @cache.write("name", "value")
760
+ assert_equal false, @cache.delete("name")
761
+ end
762
+
763
+ def test_increment
764
+ @cache.write("name", 1, :raw => true)
765
+ assert_nil @cache.increment("name")
766
+ end
767
+
768
+ def test_decrement
769
+ @cache.write("name", 1, :raw => true)
770
+ assert_nil @cache.increment("name")
771
+ end
772
+
773
+ def test_delete_matched
774
+ @cache.write("name", "value")
775
+ @cache.delete_matched(/name/)
776
+ end
777
+
778
+ def test_local_store_strategy
779
+ @cache.with_local_cache do
780
+ @cache.write("name", "value")
781
+ assert_equal "value", @cache.read("name")
782
+ @cache.delete("name")
783
+ assert_nil @cache.read("name")
784
+ @cache.write("name", "value")
785
+ end
786
+ assert_nil @cache.read("name")
787
+ end
788
+
789
+ def test_setting_nil_cache_store
790
+ assert ActiveSupport::Cache.lookup_store.class.name, ActiveSupport::Cache::NullStore.name
791
+ end
792
+ end
793
+
794
+ class CacheStoreLoggerTest < ActiveSupport::TestCase
795
+ def setup
796
+ @cache = ActiveSupport::Cache.lookup_store(:memory_store)
797
+
798
+ @buffer = StringIO.new
799
+ @cache.logger = Logger.new(@buffer)
800
+ end
801
+
802
+ def test_logging
803
+ @cache.fetch('foo') { 'bar' }
804
+ assert_present @buffer.string
805
+ end
806
+
807
+ def test_mute_logging
808
+ @cache.mute { @cache.fetch('foo') { 'bar' } }
809
+ assert_blank @buffer.string
810
+ end
811
+ end
812
+
813
+ class CacheEntryTest < ActiveSupport::TestCase
814
+ def test_create_raw_entry
815
+ time = Time.now
816
+ entry = ActiveSupport::Cache::Entry.create("raw", time, :compress => false, :expires_in => 300)
817
+ assert_equal "raw", entry.raw_value
818
+ assert_equal time.to_f, entry.created_at
819
+ assert !entry.compressed?
820
+ assert_equal 300, entry.expires_in
821
+ end
822
+
823
+ def test_expired
824
+ entry = ActiveSupport::Cache::Entry.new("value")
825
+ assert !entry.expired?, 'entry not expired'
826
+ entry = ActiveSupport::Cache::Entry.new("value", :expires_in => 60)
827
+ assert !entry.expired?, 'entry not expired'
828
+ time = Time.now + 61
829
+ Time.stubs(:now).returns(time)
830
+ assert entry.expired?, 'entry is expired'
831
+ end
832
+
833
+ def test_compress_values
834
+ entry = ActiveSupport::Cache::Entry.new("value", :compress => true, :compress_threshold => 1)
835
+ assert_equal "value", entry.value
836
+ assert entry.compressed?
837
+ assert_equal "value", Marshal.load(Zlib::Inflate.inflate(entry.raw_value))
838
+ end
839
+
840
+ def test_non_compress_values
841
+ entry = ActiveSupport::Cache::Entry.new("value")
842
+ assert_equal "value", entry.value
843
+ assert_equal "value", Marshal.load(entry.raw_value)
844
+ assert !entry.compressed?
845
+ end
846
+ end