identity_cache 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -40,6 +40,25 @@ module IdentityCache
40
40
  mattr_accessor :cache_namespace
41
41
  self.cache_namespace = "IDC:#{CACHE_VERSION}:".freeze
42
42
 
43
+ version = Gem::Version.new(IdentityCache::VERSION)
44
+
45
+ # fetch_#{association} for a cache_has_many association returns a relation
46
+ # when fetch_returns_relation is set to true and an array when set to false.
47
+ mattr_accessor :fetch_returns_relation
48
+ self.fetch_returns_relation = version < Gem::Version.new("0.4")
49
+
50
+ # Inverse active record associations are set when loading embedded
51
+ # cache_has_many associations from the cache when never_set_inverse_association
52
+ # is false. When set to true, it will only set the inverse cached association.
53
+ mattr_accessor :never_set_inverse_association
54
+ self.never_set_inverse_association = version >= Gem::Version.new("0.4")
55
+
56
+ # Fetched records are not read-only and this could sometimes prevent IDC from
57
+ # reflecting what's truly in the database when fetch_read_only_records is false.
58
+ # When set to true, it will only return read-only records when cache is used.
59
+ mattr_accessor :fetch_read_only_records
60
+ self.fetch_read_only_records = version >= Gem::Version.new("0.4")
61
+
43
62
  def included(base) #:nodoc:
44
63
  raise AlreadyIncludedError if base.include?(IdentityCache::ConfigurationDSL)
45
64
 
@@ -58,7 +77,7 @@ module IdentityCache
58
77
  # +cache_adaptor+ - A ActiveSupport::Cache::Store
59
78
  #
60
79
  def cache_backend=(cache_adaptor)
61
- if @cache
80
+ if defined?(@cache)
62
81
  cache.cache_backend = cache_adaptor
63
82
  else
64
83
  @cache = MemoizedCacheProxy.new(cache_adaptor)
@@ -131,6 +150,31 @@ module IdentityCache
131
150
  result
132
151
  end
133
152
 
153
+ def with_fetch_returns_relation(value = true)
154
+ previous_value = self.fetch_returns_relation
155
+ self.fetch_returns_relation = value
156
+ yield
157
+ ensure
158
+ self.fetch_returns_relation = previous_value
159
+ end
160
+
161
+ def with_never_set_inverse_association(value = true)
162
+ old_value = self.never_set_inverse_association
163
+ self.never_set_inverse_association = value
164
+ yield
165
+ ensure
166
+ self.never_set_inverse_association = old_value
167
+ end
168
+
169
+
170
+ def with_fetch_read_only_records(value = true)
171
+ old_value = self.fetch_read_only_records
172
+ self.fetch_read_only_records = value
173
+ yield
174
+ ensure
175
+ self.fetch_read_only_records = old_value
176
+ end
177
+
134
178
  private
135
179
 
136
180
  def fetch_in_batches(keys)
@@ -14,7 +14,7 @@ require File.dirname(__FILE__) + '/../test/helpers/database_connection'
14
14
 
15
15
  IdentityCache.logger = Logger.new(nil)
16
16
  IdentityCache.cache_backend = ActiveSupport::Cache::MemcachedStore.new("localhost:#{$memcached_port}", :support_cas => true)
17
-
17
+ ActiveRecord::Base.raise_in_transactional_callbacks = true
18
18
 
19
19
  def create_record(id)
20
20
  Item.new(id)
@@ -3,8 +3,6 @@ require 'test_helper'
3
3
  class CacheHashTest < IdentityCache::TestCase
4
4
 
5
5
  def test_memcache_hash
6
-
7
- prng = Random.new(Time.now.to_i)
8
6
  3.times do
9
7
  random_str = Array.new(200){rand(36).to_s(36)}.join
10
8
  hash_val = IdentityCache.memcache_hash(random_str)
@@ -10,6 +10,7 @@ class CacheInvalidationTest < IdentityCache::TestCase
10
10
  @record.save
11
11
  @record.reload
12
12
  @baz, @bar = @record.associated_records[0], @record.associated_records[1]
13
+ @record.reload
13
14
  end
14
15
 
15
16
  def test_reload_invalidate_cached_ids
@@ -21,7 +22,7 @@ class CacheInvalidationTest < IdentityCache::TestCase
21
22
  assert_equal [@baz.id, @bar.id], @record.instance_variable_get(variable_name)
22
23
 
23
24
  @record.reload
24
- assert_equal nil, @record.instance_variable_get(variable_name)
25
+ assert_equal false, @record.instance_variable_defined?(variable_name)
25
26
 
26
27
  @record.fetch_associated_record_ids
27
28
  assert_equal [@baz.id, @bar.id], @record.instance_variable_get(variable_name)
@@ -36,7 +37,7 @@ class CacheInvalidationTest < IdentityCache::TestCase
36
37
  assert_equal [@baz, @bar], @record.instance_variable_get(variable_name)
37
38
 
38
39
  @record.reload
39
- assert_equal nil, @record.instance_variable_get(variable_name)
40
+ assert_equal false, @record.instance_variable_defined?(variable_name)
40
41
 
41
42
  @record.fetch_associated_records
42
43
  assert_equal [@baz, @bar], @record.instance_variable_get(variable_name)
@@ -45,14 +46,14 @@ class CacheInvalidationTest < IdentityCache::TestCase
45
46
  def test_after_a_reload_the_cache_perform_as_expected
46
47
  Item.cache_has_many :associated_records, :embed => :ids
47
48
 
48
- assert_equal [@baz, @bar], @record.associated_records
49
49
  assert_equal [@baz, @bar], @record.fetch_associated_records
50
+ assert_equal [@baz, @bar], @record.associated_records
50
51
 
51
52
  @baz.destroy
52
53
  @record.reload
53
54
 
54
- assert_equal [@bar], @record.associated_records
55
55
  assert_equal [@bar], @record.fetch_associated_records
56
+ assert_equal [@bar], @record.associated_records
56
57
  end
57
58
 
58
59
  def test_cache_invalidation_expire_properly_if_child_is_embed_in_multiple_parents
@@ -12,6 +12,14 @@ class DenormalizedHasManyTest < IdentityCache::TestCase
12
12
  @record.reload
13
13
  end
14
14
 
15
+ def test_uncached_record_from_the_db_should_come_back_with_association_array_when_fetch_returns_array
16
+ IdentityCache.with_fetch_returns_relation(false) do
17
+ assert_equal IdentityCache.fetch_returns_relation, false
18
+ record_from_db = Item.find(@record.id)
19
+ assert_equal Array, record_from_db.fetch_associated_records.class
20
+ end
21
+ end
22
+
15
23
  def test_uncached_record_from_the_db_will_use_normal_association
16
24
  expected = @record.associated_records
17
25
  record_from_db = Item.find(@record.id)
@@ -22,6 +30,17 @@ class DenormalizedHasManyTest < IdentityCache::TestCase
22
30
  assert_equal expected, record_from_db.fetch_associated_records
23
31
  end
24
32
 
33
+ def test_on_cache_hit_record_should_come_back_with_cached_association_array_when_fetch_returns_array
34
+ IdentityCache.with_fetch_returns_relation(false) do
35
+ assert_equal IdentityCache.fetch_returns_relation, false
36
+ Item.fetch(@record.id) # warm cache
37
+
38
+ record_from_cache_hit = Item.fetch(@record.id)
39
+ assert_equal @record, record_from_cache_hit
40
+ assert_equal Array, record_from_cache_hit.fetch_associated_records.class
41
+ end
42
+ end
43
+
25
44
  def test_on_cache_hit_record_should_come_back_with_cached_association
26
45
  Item.fetch(@record.id) # warm cache
27
46
 
@@ -94,16 +113,91 @@ class DenormalizedHasManyTest < IdentityCache::TestCase
94
113
  child.save!
95
114
  end
96
115
 
97
- def test_unsupported_through_assocation
98
- assert_raises IdentityCache::UnsupportedAssociationError, "caching through associations isn't supported" do
99
- Item.has_many :deeply_through_associated_records, :through => :associated_records, foreign_key: 'associated_record_id', inverse_of: :item, :class_name => 'DeeplyAssociatedRecord'
100
- Item.cache_has_many :deeply_through_associated_records, :embed => true
116
+ def test_fetch_association_does_not_allow_chaining_when_fetch_returns_array
117
+ IdentityCache.with_fetch_returns_relation(false) do
118
+ check = proc { assert_equal false, Item.fetch(@record.id).fetch_associated_records.respond_to?(:where) }
119
+ 2.times { check.call } # for miss and hit
120
+ Item.transaction { check.call }
121
+ end
122
+ end
123
+
124
+ def test_never_set_inverse_association_on_cache_hit
125
+ Item.fetch(@record.id) # warm cache
126
+
127
+ item = IdentityCache.with_never_set_inverse_association do
128
+ Item.fetch(@record.id)
101
129
  end
130
+
131
+ associated_record = item.fetch_associated_records.to_a.first
132
+ assert item.object_id != associated_record.item.object_id
133
+ end
134
+
135
+ def test_deprecated_set_inverse_association_on_cache_hit
136
+ Item.fetch(@record.id) # warm cache
137
+
138
+ item = Item.fetch(@record.id)
139
+
140
+ associated_record = item.fetch_associated_records.to_a.first
141
+ assert_equal item.object_id, associated_record.item.object_id
102
142
  end
103
143
 
104
- def test_cache_has_many_on_derived_model_raises
105
- assert_raises(IdentityCache::DerivedModelError) do
106
- StiRecordTypeA.cache_has_many :polymorphic_records, :inverse_name => :owner, :embed => true
144
+ def test_returned_records_should_be_readonly_on_cache_hit
145
+ IdentityCache.with_fetch_read_only_records do
146
+ Item.fetch(@record.id) # warm cache
147
+ record_from_cache_hit = Item.fetch(@record.id)
148
+ assert record_from_cache_hit.fetch_associated_records.all?(&:readonly?)
149
+ end
150
+ end
151
+
152
+ def test_returned_records_should_be_readonly_on_cache_miss
153
+ IdentityCache.with_fetch_read_only_records do
154
+ record_from_cache_miss = Item.fetch(@record.id)
155
+ assert record_from_cache_miss.fetch_associated_records.all?(&:readonly?)
156
+ end
157
+ end
158
+
159
+ def test_db_returned_records_should_never_be_readonly
160
+ IdentityCache.with_fetch_read_only_records do
161
+ record_from_db = Item.find(@record.id)
162
+ uncached_records = record_from_db.associated_records
163
+ assert uncached_records.none?(&:readonly?)
164
+ assert record_from_db.fetch_associated_records.all?(&:readonly?)
165
+ assert record_from_db.associated_records.none?(&:readonly?)
166
+ end
167
+ end
168
+
169
+ def test_returned_records_with_open_transactions_should_not_be_readonly
170
+ IdentityCache.with_fetch_read_only_records do
171
+ Item.transaction do
172
+ assert_equal IdentityCache.should_use_cache?, false
173
+ assert Item.fetch(@record.id).fetch_associated_records.none?(&:readonly?)
174
+ end
175
+ end
176
+ end
177
+
178
+ class CheckAssociationTest < IdentityCache::TestCase
179
+ def test_unsupported_through_assocation
180
+ assert_raises IdentityCache::UnsupportedAssociationError, "caching through associations isn't supported" do
181
+ Item.has_many :deeply_through_associated_records, :through => :associated_records, foreign_key: 'associated_record_id', inverse_of: :item, :class_name => 'DeeplyAssociatedRecord'
182
+ Item.cache_has_many :deeply_through_associated_records, :embed => true
183
+ end
184
+ end
185
+
186
+ def test_unsupported_joins_in_assocation_scope
187
+ scope = -> { joins(:associated_record).where(associated_records: { name: 'contrived example' }) }
188
+ Item.has_many :deeply_joined_associated_records, scope, inverse_of: :item, class_name: 'DeeplyAssociatedRecord'
189
+ Item.cache_has_many :deeply_joined_associated_records, :embed => true
190
+
191
+ message = "caching association Item.deeply_joined_associated_records scoped with a join isn't supported"
192
+ assert_raises IdentityCache::UnsupportedAssociationError, message do
193
+ Item.fetch(1)
194
+ end
195
+ end
196
+
197
+ def test_cache_has_many_on_derived_model_raises
198
+ assert_raises(IdentityCache::DerivedModelError) do
199
+ StiRecordTypeA.cache_has_many :polymorphic_records, :inverse_name => :owner, :embed => true
200
+ end
107
201
  end
108
202
  end
109
203
  end
@@ -120,4 +120,40 @@ class DenormalizedHasOneTest < IdentityCache::TestCase
120
120
  StiRecordTypeA.cache_has_one :polymorphic_record, :inverse_name => :owner, :embed => true
121
121
  end
122
122
  end
123
+
124
+ def test_returned_record_should_be_readonly_on_cache_hit
125
+ IdentityCache.with_fetch_read_only_records do
126
+ Item.fetch_by_title('foo')
127
+ record_from_cache_hit = Item.fetch_by_title('foo')
128
+ assert record_from_cache_hit.fetch_associated.readonly?
129
+ refute record_from_cache_hit.associated.readonly?
130
+ end
131
+ end
132
+
133
+ def test_returned_record_should_be_readonly_on_cache_miss
134
+ IdentityCache.with_fetch_read_only_records do
135
+ assert IdentityCache.should_use_cache?
136
+ record_from_cache_miss = Item.fetch_by_title('foo')
137
+ assert record_from_cache_miss.fetch_associated.readonly?
138
+ end
139
+ end
140
+
141
+ def test_db_returned_record_should_never_be_readonly
142
+ IdentityCache.with_fetch_read_only_records do
143
+ record_from_db = Item.find_by_title('foo')
144
+ uncached_record = record_from_db.associated
145
+ refute uncached_record.readonly?
146
+ record_from_db.fetch_associated
147
+ refute uncached_record.readonly?
148
+ end
149
+ end
150
+
151
+ def test_returned_record_with_open_transactions_should_not_be_readonly
152
+ IdentityCache.with_fetch_read_only_records do
153
+ Item.transaction do
154
+ refute IdentityCache.should_use_cache?
155
+ refute Item.fetch_by_title('foo').fetch_associated.readonly?
156
+ end
157
+ end
158
+ end
123
159
  end
@@ -223,6 +223,48 @@ class FetchMultiTest < IdentityCache::TestCase
223
223
  end
224
224
  end
225
225
 
226
+ def test_fetch_multi_with_mixed_hits_and_misses_returns_only_readonly_records
227
+ IdentityCache.with_fetch_read_only_records do
228
+ cache_response = {}
229
+ cache_response[@bob_blob_key] = cache_response_for(@bob)
230
+ cache_response[@joe_blob_key] = nil
231
+ cache_response[@fred_blob_key] = cache_response_for(@fred)
232
+ fetch_multi = fetch_multi_stub(cache_response)
233
+
234
+ response = Item.fetch_multi(@bob.id, @joe.id, @fred.id)
235
+ assert fetch_multi.has_been_called_with?(@bob_blob_key, @joe_blob_key, @fred_blob_key)
236
+ assert_equal [@bob, @joe, @fred], response
237
+
238
+ assert response.all?(&:readonly?)
239
+ end
240
+ end
241
+
242
+ def test_fetch_multi_with_mixed_hits_and_misses_and_responses_in_the_wrong_order_returns_readonly
243
+ IdentityCache.with_fetch_read_only_records do
244
+ cache_response = {}
245
+ cache_response[@bob_blob_key] = nil
246
+ cache_response[@joe_blob_key] = nil
247
+ cache_response[@fred_blob_key] = cache_response_for(@fred)
248
+ fetch_multi = fetch_multi_stub(cache_response)
249
+
250
+ response = Item.fetch_multi(@bob.id, @joe.id, @fred.id)
251
+ assert fetch_multi.has_been_called_with?(@bob_blob_key, @joe_blob_key, @fred_blob_key)
252
+ assert_equal [@bob, @joe, @fred], response
253
+
254
+ assert response.all?(&:readonly?)
255
+ end
256
+ end
257
+
258
+ def test_fetch_multi_with_open_transactions_returns_non_readonly_records
259
+ IdentityCache.with_fetch_read_only_records do
260
+ Item.transaction do
261
+ assert_equal IdentityCache.should_use_cache?, false
262
+ IdentityCache.cache.expects(:fetch_multi).never
263
+ assert Item.fetch_multi(@bob.id, @joe.id, @fred.id).none?(&:readonly?)
264
+ end
265
+ end
266
+ end
267
+
226
268
  private
227
269
 
228
270
  def populate_only_fred
data/test/fetch_test.rb CHANGED
@@ -212,4 +212,36 @@ class FetchTest < IdentityCache::TestCase
212
212
  StiRecordTypeA.fetch(1)
213
213
  end
214
214
  end
215
+
216
+ def test_returned_records_are_readonly_on_cache_hit
217
+ IdentityCache.with_fetch_read_only_records do
218
+ IdentityCache.cache.expects(:fetch).with(@blob_key).returns(@cached_value)
219
+ assert Item.fetch(1).readonly?
220
+ end
221
+ end
222
+
223
+ def test_returned_records_are_readonly_on_cache_miss
224
+ IdentityCache.with_fetch_read_only_records do
225
+ fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
226
+ Item.expects(:resolve_cache_miss).with(1).once.returns(@record)
227
+
228
+ assert Item.exists_with_identity_cache?(1)
229
+ assert fetch.has_been_called_with?(@blob_key)
230
+ assert Item.fetch(1).readonly?
231
+ end
232
+ end
233
+
234
+ def test_returned_records_are_not_readonly_with_open_transactions
235
+ IdentityCache.with_fetch_read_only_records do
236
+
237
+ @record.transaction do
238
+ fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
239
+ Item.expects(:resolve_cache_miss).with(1).once.returns(@record)
240
+
241
+ refute IdentityCache.should_use_cache?
242
+ refute fetch.has_been_called_with?(@blob_key)
243
+ refute Item.fetch(1).readonly?, "Fetched item was read-only"
244
+ end
245
+ end
246
+ end
215
247
  end
@@ -50,14 +50,15 @@ module DatabaseConnection
50
50
  'mysql2' => {
51
51
  'adapter' => 'mysql2',
52
52
  'database' => 'identity_cache_test',
53
- 'host' => '127.0.0.1',
53
+ 'host' => ENV['MYSQL_HOST'] || '127.0.0.1',
54
54
  'username' => 'root'
55
55
  },
56
56
  'postgresql' => {
57
57
  'adapter' => 'postgresql',
58
58
  'database' => 'identity_cache_test',
59
- 'host' => '127.0.0.1',
60
- 'username' => 'postgres'
59
+ 'host' => ENV['POSTGRES_HOST'] || '127.0.0.1',
60
+ 'username' => 'postgres',
61
+ 'prepared_statements' => false,
61
62
  }
62
63
  }
63
64
  end
@@ -143,13 +143,13 @@ class IndexCacheTest < IdentityCache::TestCase
143
143
 
144
144
  def test_cache_index_with_non_id_primary_key
145
145
  KeyedRecord.cache_index :value
146
- fixture = KeyedRecord.create!(value: "a") { |r| r.hashed_key = 123 }
146
+ KeyedRecord.create!(value: "a") { |r| r.hashed_key = 123 }
147
147
  assert_equal [123], KeyedRecord.fetch_by_value('a').map(&:id)
148
148
  end
149
149
 
150
150
  def test_unique_cache_index_with_non_id_primary_key
151
151
  KeyedRecord.cache_index :value, unique: true
152
- fixture = KeyedRecord.create!(value: "a") { |r| r.hashed_key = 123 }
152
+ KeyedRecord.create!(value: "a") { |r| r.hashed_key = 123 }
153
153
  assert_equal 123, KeyedRecord.fetch_by_value('a').id
154
154
  end
155
155
 
@@ -0,0 +1,49 @@
1
+ require "test_helper"
2
+
3
+ class MemoizedAttributesTest < IdentityCache::TestCase
4
+ def test_memoization_should_not_break_dirty_tracking_with_empty_cache
5
+ item = Item.create!
6
+
7
+ IdentityCache.cache.with_memoization do
8
+ Item.fetch(item.id).title = "my title"
9
+ Item.fetch(item.id).update!(title: "my title")
10
+ end
11
+
12
+ assert_equal "my title", Item.find(item.id).title
13
+ end
14
+
15
+ def test_memoization_should_not_break_dirty_tracking_with_filled_cache
16
+ item = Item.create!
17
+
18
+ IdentityCache.cache.with_memoization do
19
+ Item.fetch(item.id)
20
+ Item.fetch(item.id).title = "my title"
21
+ Item.fetch(item.id).update!(title: "my title")
22
+ end
23
+
24
+ assert_equal "my title", Item.find(item.id).title
25
+ end
26
+
27
+ def test_memoization_with_fetch_multi_should_not_break_dirty_tracking_with_empty_cache
28
+ item = Item.create!
29
+
30
+ IdentityCache.cache.with_memoization do
31
+ Item.fetch_multi(item.id).first.title = "my title"
32
+ Item.fetch_multi(item.id).first.update!(title: "my title")
33
+ end
34
+
35
+ assert_equal "my title", Item.find(item.id).title
36
+ end
37
+
38
+ def test_memoization_with_fetch_multi_should_not_break_dirty_tracking_with_filled_cache
39
+ item = Item.create!
40
+
41
+ IdentityCache.cache.with_memoization do
42
+ Item.fetch_multi(item.id)
43
+ Item.fetch_multi(item.id).first.title = "my title"
44
+ Item.fetch_multi(item.id).first.update!(title: "my title")
45
+ end
46
+
47
+ assert_equal "my title", Item.find(item.id).title
48
+ end
49
+ end
@@ -60,4 +60,28 @@ class NormalizedBelongsToTest < IdentityCache::TestCase
60
60
 
61
61
  assert_equal @parent_record, PolymorphicRecord.first.fetch_owner
62
62
  end
63
+
64
+ def test_returned_record_should_be_readonly_on_cache_hit
65
+ IdentityCache.with_fetch_read_only_records do
66
+ @record.fetch_item # warm cache
67
+ assert @record.fetch_item.readonly?
68
+ refute @record.item.readonly?
69
+ end
70
+ end
71
+
72
+ def test_returned_record_should_be_readonly_on_cache_miss
73
+ IdentityCache.with_fetch_read_only_records do
74
+ assert @record.fetch_item.readonly?
75
+ refute @record.item.readonly?
76
+ end
77
+ end
78
+
79
+ def test_returned_record_with_open_transactions_should_not_be_readonly
80
+ IdentityCache.with_fetch_read_only_records do
81
+ Item.transaction do
82
+ refute IdentityCache.should_use_cache?
83
+ refute @record.fetch_item.readonly?
84
+ end
85
+ end
86
+ end
63
87
  end
@@ -111,9 +111,10 @@ class NormalizedHasManyTest < IdentityCache::TestCase
111
111
  def test_fetching_the_association_should_delegate_to_the_normal_association_fetcher_if_any_transaction_are_open
112
112
  @record = Item.fetch(@record.id)
113
113
 
114
- Item.expects(:fetch_multi).never
115
- @record.transaction do
116
- assert_equal [@baz, @bar], @record.fetch_associated_records
114
+ assert_memcache_operations(0) do
115
+ @record.transaction do
116
+ assert_equal [@baz, @bar], @record.fetch_associated_records
117
+ end
117
118
  end
118
119
  end
119
120
 
@@ -121,8 +122,9 @@ class NormalizedHasManyTest < IdentityCache::TestCase
121
122
  # Warm the ActiveRecord association
122
123
  @record.associated_records.to_a
123
124
 
124
- Item.expects(:fetch_multi).never
125
- assert_equal [@baz, @bar], @record.fetch_associated_records
125
+ assert_memcache_operations(0) do
126
+ assert_equal [@baz, @bar], @record.fetch_associated_records
127
+ end
126
128
  end
127
129
 
128
130
  def test_saving_a_child_record_shouldnt_expire_the_parents_blob_if_the_foreign_key_hasnt_changed
@@ -195,4 +197,26 @@ class NormalizedHasManyTest < IdentityCache::TestCase
195
197
  @not_cached.save!
196
198
  end
197
199
 
200
+ def test_fetch_association_does_not_allow_chaining_when_fetch_returns_array
201
+ IdentityCache.with_fetch_returns_relation(false) do
202
+ check = proc { assert_equal false, Item.fetch(@record.id).fetch_associated_records.respond_to?(:where) }
203
+ 2.times { check.call } # for miss and hit
204
+ Item.transaction { check.call }
205
+ end
206
+ end
207
+
208
+ def test_returned_records_should_be_readonly_on_cache_hit
209
+ IdentityCache.with_fetch_read_only_records do
210
+ Item.fetch(@record.id) # warm cache
211
+ record_from_cache_hit = Item.fetch(@record.id)
212
+ record_from_cache_hit.fetch_associated_records.all?(&:readonly?)
213
+ end
214
+ end
215
+
216
+ def test_returned_records_should_be_readonly_on_cache_miss
217
+ IdentityCache.with_fetch_read_only_records do
218
+ record_from_cache_miss = Item.fetch(@record.id)
219
+ assert record_from_cache_miss.fetch_associated_records.all?(&:readonly?)
220
+ end
221
+ end
198
222
  end