factorylabs-cache-money 0.2.5

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