mguymon-cache-money 0.2.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,382 @@
1
+ require "spec_helper"
2
+
3
+ module Cash
4
+ describe Finders do
5
+ describe 'Cache Usage' do
6
+ describe 'when the cache is populated' do
7
+ describe '#find' do
8
+ describe '#find(1)' do
9
+ it 'does not use the database' do
10
+ story = Story.create!
11
+ mock(Story.connection).execute.never
12
+ Story.find(story.id).should == story
13
+ end
14
+ end
15
+
16
+ describe '#find(object)' do
17
+ it 'uses the objects quoted id' do
18
+ story = Story.create!
19
+ mock(Story.connection).execute.never
20
+ Story.find(story).should == story
21
+ end
22
+ end
23
+
24
+ describe '#find(:first, ...)' do
25
+ describe '#find(:first, :conditions => { :id => ? })' do
26
+ it "does not use the database" do
27
+ story = Story.create!
28
+ mock(Story.connection).execute.never
29
+ Story.find(:first, :conditions => { :id => story.id }).should == story
30
+ end
31
+ end
32
+
33
+ describe '#find(:first, :conditions => [ "id = :id", { :id => story.id } ])' do
34
+ it "does not use the database" do
35
+ story = Story.create!
36
+ mock(Story.connection).execute.never
37
+ Story.find(:first, :conditions => [ "id = :id", { :id => story.id } ]).should == story
38
+ end
39
+ end
40
+
41
+ describe "#find(:first, :conditions => 'id = ?')" do
42
+ it "does not use the database" do
43
+ story = Story.create!
44
+ mock(Story.connection).execute.never
45
+ Story.find(:first, :conditions => "id = #{story.id}").should == story
46
+ Story.find(:first, :conditions => "`stories`.id = #{story.id}").should == story
47
+ Story.find(:first, :conditions => "`stories`.`id` = #{story.id}").should == story
48
+ end
49
+ end
50
+
51
+ describe '#find(:first, :readonly => false) and any other options other than conditions are nil' do
52
+ it "does not use the database" do
53
+ story = Story.create!
54
+ mock(Story.connection).execute.never
55
+ Story.find(:first, :conditions => { :id => story.id }, :readonly => false, :limit => nil, :offset => nil, :joins => nil, :include => nil).should == story
56
+ end
57
+ end
58
+
59
+ describe '#find(:first, :readonly => true)' do
60
+ it "uses the database, not the cache" do
61
+ story = Story.create!
62
+ mock(Story).get.never
63
+ Story.find(:first, :conditions => { :id => story.id }, :readonly => true).should == story
64
+ end
65
+ end
66
+
67
+ describe '#find(:first, :join => ...) or find(..., :include => ...)' do
68
+ it "uses the database, not the cache" do
69
+ story = Story.create!
70
+ mock(Story).get.never
71
+ Story.find(:first, :conditions => { :id => story.id }, :joins => 'AS stories').should == story
72
+ Story.find(:first, :conditions => { :id => story.id }, :include => :characters).should == story
73
+ end
74
+ end
75
+
76
+ describe '#find(:first)' do
77
+ it 'uses the database, not the cache' do
78
+ mock(Story).get.never
79
+ Story.find(:first)
80
+ end
81
+ end
82
+
83
+ describe '#find(:first, :conditions => "...")' do
84
+ describe 'on unindexed attributes' do
85
+ it 'uses the database, not the cache' do
86
+ story = Story.create!
87
+ mock(Story).get.never
88
+ Story.find(:first, :conditions => "type IS NULL")
89
+ end
90
+ end
91
+
92
+ describe 'on indexed attributes' do
93
+ describe 'when the attributes are integers' do
94
+ it 'does not use the database' do
95
+ story = Story.create!
96
+ mock(Story.connection).execute.never
97
+ Story.find(:first, :conditions => "`stories`.id = #{story.id}") \
98
+ .should == story
99
+ end
100
+ end
101
+
102
+ describe 'when the attributes are non-integers' do
103
+ it 'uses the database, not the cache' do
104
+ story = Story.create!(:title => "title")
105
+ mock(Story.connection).execute.never
106
+ Story.find(:first, :conditions => "`stories`.title = '#{story.title }'") \
107
+ .should == story
108
+ end
109
+ end
110
+
111
+ describe 'when the attributes must be coerced to sql values' do
112
+ it 'does not use the database' do
113
+ story1 = Story.create!(:published => true)
114
+ story2 = Story.create!(:published => false)
115
+ mock(Story.connection).execute.never
116
+ Story.find(:first, :conditions => 'published = 0').should == story2
117
+ end
118
+ end
119
+ end
120
+
121
+ describe '#find(:first, :conditions => [...])' do
122
+ describe 'with one indexed attribute' do
123
+ it 'does not use the database' do
124
+ story = Story.create!
125
+ mock(Story.connection).execute.never
126
+ Story.find(:first, :conditions => ['id = ?', story.id]).should == story
127
+ end
128
+ end
129
+
130
+ describe 'with two attributes that match a combo-index' do
131
+ it 'does not use the database' do
132
+ story = Story.create!(:title => 'title')
133
+ mock(Story.connection).execute.never
134
+ Story.find(:first, :conditions => ['id = ? AND title = ?', story.id, story.title]).should == story
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ describe '#find(:first, :conditions => {...})' do
141
+ it "does not use the database" do
142
+ story = Story.create!(:title => "Sam")
143
+ mock(Story.connection).execute.never
144
+ Story.find(:first, :conditions => { :id => story.id, :title => story.title }).should == story
145
+ end
146
+
147
+ describe 'regardless of hash order' do
148
+ it 'does not use the database' do
149
+ story = Story.create!(:title => "Sam")
150
+ mock(Story.connection).execute.never
151
+ Story.find(:first, :conditions => { :id => story.id, :title => story.title }).should == story
152
+ Story.find(:first, :conditions => { :title => story.title, :id => story.id }).should == story
153
+ end
154
+ end
155
+
156
+ describe 'on unindexed attribtes' do
157
+ it 'uses the database, not the cache' do
158
+ story = Story.create!
159
+ mock(Story).get.never
160
+ Story.find(:first, :conditions => { :id => story.id, :type => story.type }).should == story
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ describe 'when there is a with_scope' do
167
+ describe 'when the with_scope has conditions' do
168
+ describe 'when the scope conditions is a string' do
169
+ it "uses the database, not the cache" do
170
+ story = Story.create!(:title => title = 'title')
171
+ mock(Story.connection).execute.never
172
+ Story.send :with_scope, :find => { :conditions => "title = '#{title}'"} do
173
+ Story.find(:first, :conditions => { :id => story.id }).should == story
174
+ end
175
+ end
176
+ end
177
+
178
+ describe 'when the find conditions is a string' do
179
+ it "does not use the database" do
180
+ story = Story.create!(:title => title = 'title')
181
+ mock(Story.connection).execute.never
182
+ Story.send :with_scope, :find => { :conditions => { :id => story.id }} do
183
+ Story.find(:first, :conditions => "title = '#{title}'").should == story
184
+ end
185
+ end
186
+ end
187
+
188
+ describe '#find(1, :conditions => ...)' do
189
+ it "does not use the database" do
190
+ story = Story.create!
191
+ character = Character.create!(:name => name = 'barbara', :story_id => story)
192
+ mock(Character.connection).execute.never
193
+ Character.send :with_scope, :find => { :conditions => { :story_id => story.id } } do
194
+ Character.find(character.id, :conditions => { :name => name }).should == character
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ describe 'has_many associations' do
201
+ describe '#find(1)' do
202
+ it "does not use the database" do
203
+ story = Story.create!
204
+ character = story.characters.create!
205
+ mock(Character.connection).execute.never
206
+ story.characters.find(character.id).should == character
207
+ end
208
+ end
209
+
210
+ describe '#find(1, 2, ...)' do
211
+ it "does not use the database" do
212
+ story = Story.create!
213
+ character1 = story.characters.create!
214
+ character2 = story.characters.create!
215
+ mock(Character.connection).execute.never
216
+ story.characters.find(character1.id, character2.id).should == [character1, character2]
217
+ end
218
+ end
219
+
220
+ describe '#find_by_attr' do
221
+ it "does not use the database" do
222
+ story = Story.create!
223
+ character = story.characters.create!
224
+ mock(Character.connection).execute.never
225
+ story.characters.find_by_id(character.id).should == character
226
+ end
227
+ end
228
+ end
229
+ end
230
+
231
+ describe '#find(:all)' do
232
+ it "uses the database, not the cache" do
233
+ character = Character.create!
234
+ mock(Character).get.never
235
+ Character.find(:all).should == [character]
236
+ end
237
+
238
+ describe '#find(:all, :conditions => {...})' do
239
+ describe 'when the index is not empty' do
240
+ it 'does not use the database' do
241
+ story1 = Story.create!(:title => title = "title")
242
+ story2 = Story.create!(:title => title)
243
+ mock(Story.connection).execute.never
244
+ Story.find(:all, :conditions => { :title => story1.title }).should == [story1, story2]
245
+ end
246
+ end
247
+ end
248
+
249
+ describe '#find(:all, :limit => ..., :offset => ...)' do
250
+ it "cached attributes should support limits and offsets" do
251
+ character1 = Character.create!(:name => "Sam", :story_id => 1)
252
+ character2 = Character.create!(:name => "Sam", :story_id => 1)
253
+ character3 = Character.create!(:name => "Sam", :story_id => 1)
254
+ mock(Character.connection).execute.never
255
+
256
+ Character.find(:all, :conditions => { :name => character1.name, :story_id => character1.story_id }, :limit => 1).should == [character1]
257
+ Character.find(:all, :conditions => { :name => character1.name, :story_id => character1.story_id }, :offset => 1).should == [character2, character3]
258
+ Character.find(:all, :conditions => { :name => character1.name, :story_id => character1.story_id }, :limit => 1, :offset => 1).should == [character2]
259
+ end
260
+ end
261
+ end
262
+
263
+ describe '#find([...])' do
264
+ describe '#find([1, 2, ...], :conditions => ...)' do
265
+ it "uses the database, not the cache" do
266
+ story1, story2 = Story.create!, Story.create!
267
+ mock(Story).get.never
268
+ Story.find([story1.id, story2.id], :conditions => "type IS NULL").should == [story1, story2]
269
+ end
270
+ end
271
+
272
+ describe '#find([1], :conditions => ...)' do
273
+ it "uses the database, not the cache" do
274
+ story1, story2 = Story.create!, Story.create!
275
+ mock(Story).get.never
276
+ Story.find([story1.id], :conditions => "type IS NULL").should == [story1]
277
+ end
278
+ end
279
+ end
280
+
281
+ describe '#find_by_attr' do
282
+ describe 'on indexed attributes' do
283
+ describe '#find_by_id(id)' do
284
+ it "does not use the database" do
285
+ story = Story.create!
286
+ mock(Story.connection).execute.never
287
+ Story.find_by_id(story.id).should == story
288
+ end
289
+ end
290
+
291
+ describe '#find_by_title(title)' do
292
+ it "does not use the database" do
293
+ story1 = Story.create!(:title => 'title1')
294
+ story2 = Story.create!(:title => 'title2')
295
+ mock(Story.connection).execute.never
296
+ Story.find_by_title('title1').should == story1
297
+ end
298
+ end
299
+ end
300
+ end
301
+
302
+ describe "Single Table Inheritence" do
303
+ describe '#find(:all, ...)' do
304
+ it "does not use the database" do
305
+ story, epic, oral = Story.create!(:title => title = 'foo'), Epic.create!(:title => title), Oral.create!(:title => title)
306
+ mock(Story.connection).execute.never
307
+ Story.find(:all, :conditions => { :title => title }).should == [story, epic, oral]
308
+ Epic.find(:all, :conditions => { :title => title }).should == [epic, oral]
309
+ Oral.find(:all, :conditions => { :title => title }).should == [oral]
310
+ end
311
+ end
312
+ end
313
+ end
314
+
315
+ describe '#without_cache' do
316
+ describe 'when finders are called within the provided block' do
317
+ it 'uses the database not the cache' do
318
+ story = Story.create!
319
+ mock(Story).get.never
320
+ Story.without_cache do
321
+ Story.find(story.id).should == story
322
+ end
323
+ end
324
+ end
325
+ end
326
+ end
327
+
328
+ describe 'when the cache is not populated' do
329
+ before do
330
+ @story = Story.create!(:title => 'title')
331
+ $memcache.flush_all
332
+ end
333
+
334
+ describe '#find(:first, ...)' do
335
+ it 'populates the cache' do
336
+ Story.find(:first, :conditions => { :title => @story.title })
337
+ Story.fetch("title/#{@story.title}").should == [@story.id]
338
+ end
339
+ end
340
+
341
+ describe '#find_by_attr' do
342
+ before(:each) do
343
+ Story.find_by_title(@story.title) # populates cache for title with [@story.id]
344
+ end
345
+
346
+ it 'populates the cache' do
347
+ Story.fetch("title/#{@story.title}").should == [@story.id]
348
+ end
349
+
350
+ it 'populates the cache when finding by non-primary-key attribute' do
351
+ Story.find_by_title(@story.title) # populates cache for id with record
352
+
353
+ mock(Story.connection).execute.never
354
+ Story.find_by_title(@story.title).id.should == @story.id # should hit cache only
355
+ end
356
+ end
357
+
358
+ describe '#find(:all, :conditions => ...)' do
359
+ it 'populates the cache' do
360
+ Story.find(:all, :conditions => { :title => @story.title })
361
+ Story.fetch("title/#{@story.title}").should == [@story.id]
362
+ end
363
+ end
364
+
365
+ describe '#find(1)' do
366
+ it 'populates the cache' do
367
+ Story.find(@story.id)
368
+ Story.fetch("id/#{@story.id}").should == [@story]
369
+ end
370
+ end
371
+
372
+ describe 'when there is a with_scope' do
373
+ it "uses the database, not the cache" do
374
+ Story.send :with_scope, :find => { :conditions => { :title => @story.title }} do
375
+ Story.find(:first, :conditions => { :id => @story.id }).should == @story
376
+ end
377
+ end
378
+ end
379
+ end
380
+ end
381
+ end
382
+ end
@@ -0,0 +1,108 @@
1
+ require "spec_helper"
2
+
3
+ module Cash
4
+ describe Lock do
5
+ describe '#synchronize' do
6
+ it "yields the block" do
7
+ block_was_called = false
8
+ $lock.synchronize('lock_key') do
9
+ block_was_called = true
10
+ end
11
+ block_was_called.should == true
12
+ end
13
+
14
+ it "acquires the specified lock before the block is run" do
15
+ $memcache.get("lock/lock_key").should == nil
16
+ $lock.synchronize('lock_key') do
17
+ $memcache.get("lock/lock_key").should_not == nil
18
+ end
19
+ end
20
+
21
+ it "releases the lock after the block is run" do
22
+ $memcache.get("lock/lock_key").should == nil
23
+ $lock.synchronize('lock_key') {}
24
+ $memcache.get("lock/lock_key").should == nil
25
+ end
26
+
27
+ it "releases the lock even if the block raises" do
28
+ $memcache.get("lock/lock_key").should == nil
29
+ $lock.synchronize('lock_key') { raise } rescue nil
30
+ $memcache.get("lock/lock_key").should == nil
31
+ end
32
+
33
+ it "does not block on recursive lock acquisition" do
34
+ $lock.synchronize('lock_key') do
35
+ lambda { $lock.synchronize('lock_key') {} }.should_not raise_error
36
+ end
37
+ end
38
+ end
39
+
40
+ describe '#acquire_lock' do
41
+ it "creates a lock at a given cache key" do
42
+ $memcache.get("lock/lock_key").should == nil
43
+ $lock.acquire_lock("lock_key")
44
+ $memcache.get("lock/lock_key").should_not == nil
45
+ end
46
+
47
+ describe 'when given a timeout for the lock' do
48
+ it "correctly sets timeout on memcache entries" do
49
+ mock($memcache).add('lock/lock_key', "#{Socket.gethostname} #{Process.pid}", timeout = 10) { true }
50
+ # $lock.acquire_lock('lock_key', timeout)
51
+ lambda { $lock.acquire_lock('lock_key', timeout, 1) }.should raise_error
52
+ end
53
+ end
54
+
55
+ describe 'when to processes contend for a lock' do
56
+ it "prevents two processes from acquiring the same lock at the same time" do
57
+ $lock.acquire_lock('lock_key')
58
+ as_another_process do
59
+ stub($lock).exponential_sleep
60
+ lambda { $lock.acquire_lock('lock_key') }.should raise_error
61
+ end
62
+ end
63
+
64
+ describe 'when given a number of times to retry' do
65
+ it "retries specified number of times" do
66
+ $lock.acquire_lock('lock_key')
67
+ as_another_process do
68
+ mock($memcache).add("lock/lock_key", "#{Socket.gethostname} #{Process.pid}", timeout = 10) { false }.times(retries = 3)
69
+ stub($lock).exponential_sleep
70
+ lambda { $lock.acquire_lock('lock_key', timeout, retries) }.should raise_error
71
+ end
72
+ end
73
+ end
74
+
75
+ describe 'when given an initial wait' do
76
+ it 'sleeps exponentially starting with the initial wait' do
77
+ stub($lock).sleep(initial_wait = 0.123)
78
+ stub($lock).sleep(2 * initial_wait)
79
+ stub($lock).sleep(4 * initial_wait)
80
+ stub($lock).sleep(8 * initial_wait)
81
+ $lock.acquire_lock('lock_key')
82
+ as_another_process do
83
+ lambda { $lock.acquire_lock('lock_key', Lock::DEFAULT_EXPIRY, Lock::DEFAULT_RETRY, initial_wait) }.should raise_error
84
+ end
85
+ end
86
+ end
87
+
88
+ def as_another_process
89
+ current_pid = Process.pid
90
+ stub(Process).pid { current_pid + 1 }
91
+ yield
92
+ end
93
+
94
+ end
95
+
96
+ end
97
+
98
+ describe '#release_lock' do
99
+ it "deletes the lock for a given cache key" do
100
+ $memcache.get("lock/lock_key").should == nil
101
+ $lock.acquire_lock("lock_key")
102
+ $memcache.get("lock/lock_key").should_not == nil
103
+ $lock.release_lock("lock_key")
104
+ $memcache.get("lock/lock_key").should == nil
105
+ end
106
+ end
107
+ end
108
+ end