lrucache 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Guardfile +1 -0
- data/README.md +1 -1
- data/lib/lrucache.rb +35 -23
- data/lib/lrucache/version.rb +1 -1
- data/spec/lrucache_spec.rb +449 -80
- metadata +3 -3
data/Guardfile
CHANGED
@@ -10,5 +10,6 @@ guard 'rspec', :cli => '-c --format documentation -r ./spec/spec_helper.rb',
|
|
10
10
|
:version => 2 do
|
11
11
|
watch(%r{^spec/.+_spec\.rb})
|
12
12
|
watch(%r{^lib/(.+)\.rb}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
13
|
+
watch('lib/lrucache.rb') { "spec/lrucache_spec.rb" }
|
13
14
|
watch('spec/spec_helper.rb') { "spec" }
|
14
15
|
end
|
data/README.md
CHANGED
data/lib/lrucache.rb
CHANGED
@@ -4,13 +4,14 @@ require "priority_queue"
|
|
4
4
|
# Not thread-safe!
|
5
5
|
class LRUCache
|
6
6
|
|
7
|
-
attr_reader :default, :max_size
|
7
|
+
attr_reader :default, :max_size, :ttl
|
8
8
|
|
9
9
|
def initialize(opts={})
|
10
|
-
@max_size = (opts[:max_size] || 100)
|
10
|
+
@max_size = Integer(opts[:max_size] || 100)
|
11
11
|
@default = opts[:default]
|
12
|
-
@
|
13
|
-
raise "max_size must be
|
12
|
+
@ttl = Float(opts[:ttl] || 0)
|
13
|
+
raise "max_size must be greater than zero" unless @max_size > 0
|
14
|
+
raise "ttl must be positive or zero" unless @ttl >= 0
|
14
15
|
@pqueue = PriorityQueue.new
|
15
16
|
@data = {}
|
16
17
|
@counter = 0
|
@@ -19,6 +20,7 @@ class LRUCache
|
|
19
20
|
def clear
|
20
21
|
@data.clear
|
21
22
|
@pqueue.delete_min until @pqueue.empty?
|
23
|
+
@counter = 0 #might as well
|
22
24
|
end
|
23
25
|
|
24
26
|
def include?(key)
|
@@ -34,38 +36,48 @@ class LRUCache
|
|
34
36
|
end
|
35
37
|
end
|
36
38
|
|
37
|
-
def store(key, value,
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
expires
|
39
|
+
def store(key, value, ttl=nil)
|
40
|
+
evict_lru! unless @data.include?(key) || @data.size < @max_size
|
41
|
+
ttl ||= @ttl
|
42
|
+
expires =
|
43
|
+
if ttl.is_a?(Time)
|
44
|
+
ttl
|
44
45
|
else
|
45
|
-
|
46
|
-
(
|
46
|
+
ttl = Float(ttl)
|
47
|
+
(ttl > 0) ? (Time.now + ttl) : nil
|
47
48
|
end
|
48
|
-
@data[key] = [value,
|
49
|
+
@data[key] = [value, expires]
|
49
50
|
access(key)
|
50
51
|
end
|
51
52
|
|
52
53
|
alias :[]= :store
|
53
54
|
|
54
|
-
def fetch(key)
|
55
|
+
def fetch(key, ttl=nil)
|
55
56
|
datum = @data[key]
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
57
|
+
unless datum.nil?
|
58
|
+
value, expires = datum
|
59
|
+
if expires.nil? || expires > Time.now # no expiration, or not expired
|
60
|
+
access(key)
|
61
|
+
return value
|
62
|
+
else # expired
|
63
|
+
delete(key)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
if block_given?
|
67
|
+
value = yield
|
68
|
+
store(key, value, ttl)
|
60
69
|
value
|
61
|
-
else
|
62
|
-
delete(key)
|
70
|
+
else
|
63
71
|
@default
|
64
72
|
end
|
65
73
|
end
|
66
74
|
|
67
75
|
alias :[] :fetch
|
68
76
|
|
77
|
+
def empty?
|
78
|
+
size == 0
|
79
|
+
end
|
80
|
+
|
69
81
|
def size
|
70
82
|
@data.size
|
71
83
|
end
|
@@ -75,13 +87,13 @@ class LRUCache
|
|
75
87
|
end
|
76
88
|
|
77
89
|
def delete(key)
|
78
|
-
@data.delete(key)
|
79
90
|
@pqueue.delete(key)
|
91
|
+
@data.delete(key)
|
80
92
|
end
|
81
93
|
|
82
94
|
private
|
83
95
|
|
84
|
-
def
|
96
|
+
def evict_lru!
|
85
97
|
key, priority = @pqueue.delete_min
|
86
98
|
@data.delete(key) unless priority.nil?
|
87
99
|
end
|
data/lib/lrucache/version.rb
CHANGED
data/spec/lrucache_spec.rb
CHANGED
@@ -1,102 +1,471 @@
|
|
1
1
|
describe LRUCache do
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
2
|
+
describe ".new" do
|
3
|
+
|
4
|
+
it "should default :max_size to 100" do
|
5
|
+
LRUCache.new.max_size.should == 100
|
6
|
+
end
|
7
|
+
it "should accept a :max_size parameter" do
|
8
|
+
LRUCache.new(:max_size => 7).max_size.should == 7
|
9
|
+
end
|
10
|
+
it "should raise an exception if :max_size parameter can't be converted to an integer" do
|
11
|
+
expect { LRUCache.new(:max_size => "moocow") }.to raise_exception
|
12
|
+
end
|
13
|
+
it "should raise an exception if :max_size parameter is converted to a negative integer" do
|
14
|
+
expect { LRUCache.new(:max_size => -1) }.to raise_exception
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should default :default to nil" do
|
18
|
+
LRUCache.new.default.should be_nil
|
19
|
+
end
|
20
|
+
it "should accept a :default parameter" do
|
21
|
+
default = double(:default)
|
22
|
+
LRUCache.new(:default => default).default.should == default
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should default :ttl to 0 (no expiration)" do
|
26
|
+
LRUCache.new.ttl.should == 0
|
27
|
+
end
|
28
|
+
it "should accept a :ttl parameter" do
|
29
|
+
LRUCache.new(:ttl => 98.6).ttl.should == 98.6
|
30
|
+
end
|
31
|
+
it "should raise an exception if :ttl parameter can't be converted to a float" do
|
32
|
+
expect { LRUCache.new(:ttl => "moocow") }.to raise_exception
|
33
|
+
end
|
34
|
+
it "should raise an exception if :ttl parameter is converted to a negative float" do
|
35
|
+
expect { LRUCache.new(:ttl => -1) }.to raise_exception
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should initially be empty" do
|
39
|
+
LRUCache.new.should be_empty
|
7
40
|
end
|
8
|
-
c.size.should == 7
|
9
41
|
end
|
10
42
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
43
|
+
describe ".clear" do
|
44
|
+
it "should empty the hash and the priority queue" do
|
45
|
+
c = LRUCache.new
|
46
|
+
10.times { c[rand(2**16)] = :x }
|
47
|
+
c.should_not be_empty
|
48
|
+
c.instance_variable_get(:@data).should_not be_empty
|
49
|
+
c.instance_variable_get(:@pqueue).should_not be_empty
|
50
|
+
|
51
|
+
c.clear
|
52
|
+
|
53
|
+
c.should be_empty
|
54
|
+
c.instance_variable_get(:@data).should be_empty
|
55
|
+
c.instance_variable_get(:@pqueue).should be_empty
|
56
|
+
end
|
25
57
|
end
|
26
58
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
59
|
+
describe ".include?(key)" do
|
60
|
+
before(:each) { @cache = LRUCache.new }
|
61
|
+
context "when the key is not present" do
|
62
|
+
it "should return false" do
|
63
|
+
@cache.include?(:a).should be_false
|
64
|
+
end
|
65
|
+
it "should not affect priorities" do
|
66
|
+
@cache.should_not_receive(:access)
|
67
|
+
@cache.include?(:a)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
context "when the key is present, but the value has expired" do
|
71
|
+
before(:each) do
|
72
|
+
now = Time.now
|
73
|
+
Timecop.freeze(now) { @cache.store(:a, 'a', now + 10) }
|
74
|
+
Timecop.freeze(now + 20)
|
75
|
+
end
|
76
|
+
after(:each) do
|
77
|
+
Timecop.return
|
78
|
+
end
|
79
|
+
it "should return false" do
|
80
|
+
@cache.include?(:a).should be_false
|
81
|
+
end
|
82
|
+
it "should delete the key" do
|
83
|
+
@cache.should_receive(:delete).with(:a)
|
84
|
+
@cache.include?(:a)
|
85
|
+
end
|
86
|
+
it "should not affect priorities" do
|
87
|
+
@cache.should_not_receive(:access)
|
88
|
+
@cache.include?(:a)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
context "when the key is present, and the value has no expiration" do
|
92
|
+
before(:each) do
|
93
|
+
@cache.store(:a, 'a', nil)
|
94
|
+
end
|
95
|
+
it "should return true" do
|
96
|
+
@cache.include?(:a).should be_true
|
97
|
+
end
|
98
|
+
it "should update the key's access stamp" do
|
99
|
+
@cache.should_receive(:access).with(:a)
|
100
|
+
@cache.include?(:a)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
context "when the key is present, and the value has not yet expired" do
|
104
|
+
before(:each) do
|
105
|
+
now = Time.now
|
106
|
+
Timecop.freeze(now)
|
107
|
+
@cache.store(:a, 'a', now + 10)
|
108
|
+
end
|
109
|
+
after(:each) do
|
110
|
+
Timecop.return
|
111
|
+
end
|
112
|
+
it "should return true" do
|
113
|
+
@cache.include?(:a).should be_true
|
114
|
+
end
|
115
|
+
it "should update the key's access stamp" do
|
116
|
+
@cache.should_receive(:access).with(:a)
|
117
|
+
@cache.include?(:a)
|
118
|
+
end
|
119
|
+
end
|
37
120
|
end
|
38
121
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
122
|
+
describe ".store(key, value, ttl=nil)" do
|
123
|
+
context "regarding evictions" do
|
124
|
+
before(:each) do
|
125
|
+
@cache = LRUCache.new(:max_size => 2)
|
126
|
+
end
|
127
|
+
context "when the cache is not full" do
|
128
|
+
context "and the key is not present" do
|
129
|
+
it "should not evict an entry" do
|
130
|
+
@cache.should_not_receive(:evict_lru!)
|
131
|
+
@cache.store(:a, 'a')
|
132
|
+
end
|
133
|
+
it "should store the value" do
|
134
|
+
@cache.store(:a, 'a')
|
135
|
+
@cache.fetch(:a).should == 'a'
|
136
|
+
end
|
137
|
+
it "should update the key's access stamp" do
|
138
|
+
@cache.should_receive(:access).with(:a)
|
139
|
+
@cache.store(:a, 'a')
|
140
|
+
end
|
141
|
+
end
|
142
|
+
context "and the key is present" do
|
143
|
+
it "should not evict an entry" do
|
144
|
+
@cache.should_not_receive(:evict_lru!)
|
145
|
+
@cache.store(:a, 'a')
|
146
|
+
end
|
147
|
+
it "should store the value" do
|
148
|
+
@cache.store(:a, 'a')
|
149
|
+
@cache.fetch(:a).should == 'a'
|
150
|
+
end
|
151
|
+
it "should update the key's access stamp" do
|
152
|
+
@cache.should_receive(:access).with(:a)
|
153
|
+
@cache.store(:a, 'a')
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
context "when the cache is full" do
|
158
|
+
before(:each) do
|
159
|
+
@cache = LRUCache.new(:max_size => 2)
|
160
|
+
@cache[:b] = 'b'
|
161
|
+
@cache[:c] = 'c'
|
162
|
+
@lru = :b
|
163
|
+
end
|
164
|
+
context "and the key is not present" do
|
165
|
+
it "should evict the least-recently used entry from the cache" do
|
166
|
+
@cache.keys.should include(@lru)
|
167
|
+
@cache.store(:a, 'a')
|
168
|
+
@cache.keys.should_not include(@lru)
|
169
|
+
end
|
170
|
+
it "should store the value" do
|
171
|
+
@cache.store(:a, 'a')
|
172
|
+
@cache.fetch(:a).should == 'a'
|
173
|
+
end
|
174
|
+
it "should update the key's access stamp" do
|
175
|
+
@cache.should_receive(:access).with(:a)
|
176
|
+
@cache.store(:a, 'a')
|
177
|
+
end
|
178
|
+
end
|
179
|
+
context "and the key is present" do
|
180
|
+
it "should not evict an entry" do
|
181
|
+
@cache.should_not_receive(:evict_lru!)
|
182
|
+
@cache.store(:c, 'c')
|
183
|
+
end
|
184
|
+
it "should store the value" do
|
185
|
+
@cache.store(:c, 'c')
|
186
|
+
@cache.fetch(:c).should == 'c'
|
187
|
+
end
|
188
|
+
it "should update the key's access stamp" do
|
189
|
+
@cache.should_receive(:access).with(:c)
|
190
|
+
@cache.store(:c, 'c')
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
context "when ttl is not given and the cache's default ttl is zero" do
|
196
|
+
it "should set the entry with no expiration time" do
|
197
|
+
c = LRUCache.new(:ttl => 0)
|
198
|
+
c.store(:a,'a')
|
199
|
+
stored = c.instance_variable_get(:@data)[:a]
|
200
|
+
stored.should == ['a', nil]
|
201
|
+
end
|
202
|
+
end
|
203
|
+
context "when ttl is not given and the cache's default ttl is greater than zero" do
|
204
|
+
it "should set the entry to expire that many seconds in the future" do
|
205
|
+
c = LRUCache.new(:ttl => 1)
|
206
|
+
now = Time.now
|
207
|
+
Timecop.freeze(now) { c.store(:a,'a') }
|
208
|
+
stored = c.instance_variable_get(:@data)[:a]
|
209
|
+
stored.last.should == now + 1
|
210
|
+
end
|
211
|
+
end
|
212
|
+
context "when ttl is a Time" do
|
213
|
+
it "should set the entry to expire at the given time" do
|
214
|
+
c = LRUCache.new
|
215
|
+
ttl = Time.now + 246
|
216
|
+
c.store(:a, 'a', ttl)
|
217
|
+
stored = c.instance_variable_get(:@data)[:a]
|
218
|
+
stored.last.should == ttl
|
219
|
+
end
|
220
|
+
end
|
221
|
+
context "when ttl can be parsed as a float" do
|
222
|
+
it "should set the entry to expire that many seconds in the future" do
|
223
|
+
c = LRUCache.new
|
224
|
+
now = Time.now
|
225
|
+
Timecop.freeze(now) { c.store(:a, 'a', "98.6") }
|
226
|
+
stored = c.instance_variable_get(:@data)[:a]
|
227
|
+
stored.last.should == now + 98.6
|
228
|
+
end
|
229
|
+
end
|
230
|
+
context "when ttl cannot be parsed as a float" do
|
231
|
+
it "should raise an exception" do
|
232
|
+
c = LRUCache.new
|
233
|
+
expect { c.store(:a, 'a', "moocow") }.to raise_exception
|
234
|
+
end
|
68
235
|
end
|
69
|
-
c[:a].should be_nil
|
70
|
-
c[:b].should be_nil
|
71
|
-
c[:c].should be_nil
|
72
|
-
c.size.should == 0
|
73
236
|
end
|
74
237
|
|
75
|
-
|
76
|
-
|
77
|
-
|
238
|
+
describe ".fetch(key, ttl=nil)" do
|
239
|
+
context "when no block is given" do
|
240
|
+
context "and the key does not exist" do
|
241
|
+
before(:each) do
|
242
|
+
@default = double(:default)
|
243
|
+
@cache = LRUCache.new(:default => @default)
|
244
|
+
end
|
245
|
+
it "should return the default value" do
|
246
|
+
@cache.fetch(:a).should == @default
|
247
|
+
end
|
248
|
+
it "should not affect the priorities" do
|
249
|
+
@cache.should_not_receive(:access)
|
250
|
+
@cache.fetch(:a)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
context "and the key has been evicted" do
|
254
|
+
before(:each) do
|
255
|
+
@cache = LRUCache.new(:max_size => 2)
|
256
|
+
@cache[:a] = 'a'
|
257
|
+
@cache[:b] = 'b'
|
258
|
+
@cache[:c] = 'c'
|
259
|
+
end
|
260
|
+
it "should return the default value" do
|
261
|
+
@cache.fetch(:a).should == @default
|
262
|
+
end
|
263
|
+
it "should not affect the priorities" do
|
264
|
+
@cache.should_not_receive(:access)
|
265
|
+
@cache.fetch(:a)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
context "and the key has expired" do
|
269
|
+
before(:each) do
|
270
|
+
@cache = LRUCache.new(:ttl => 10)
|
271
|
+
now = Time.now
|
272
|
+
Timecop.freeze(now) { @cache[:a] = 'a' }
|
273
|
+
Timecop.freeze(now + 20)
|
274
|
+
end
|
275
|
+
after(:each) do
|
276
|
+
Timecop.return
|
277
|
+
end
|
278
|
+
it "should return the default value" do
|
279
|
+
@cache.fetch(:a).should == @default
|
280
|
+
end
|
281
|
+
it "should not affect the priorities" do
|
282
|
+
@cache.should_not_receive(:access)
|
283
|
+
@cache.fetch(:a)
|
284
|
+
end
|
285
|
+
it "should delete the key" do
|
286
|
+
@cache.should_receive(:delete).with(:a)
|
287
|
+
@cache.fetch(:a)
|
288
|
+
end
|
289
|
+
end
|
290
|
+
context "and the key is present and un-expired" do
|
291
|
+
before(:each) do
|
292
|
+
@cache = LRUCache.new(:ttl => nil)
|
293
|
+
@cache[:a] = 'a'
|
294
|
+
end
|
295
|
+
it "should return the cached value" do
|
296
|
+
@cache.fetch(:a).should == 'a'
|
297
|
+
end
|
298
|
+
it "should update the key's access stamp" do
|
299
|
+
@cache.should_receive(:access).with(:a)
|
300
|
+
@cache.fetch(:a)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
context "when a block is given" do
|
305
|
+
context "when the key does not exist" do
|
306
|
+
it "should call the block and store and return the result" do
|
307
|
+
c = LRUCache.new
|
308
|
+
ttl = double(:ttl)
|
309
|
+
result = double(:result)
|
310
|
+
c.should_receive(:store).with(:a, result, ttl)
|
311
|
+
c.fetch(:a, ttl){ result }.should == result
|
312
|
+
end
|
313
|
+
end
|
314
|
+
context "when the key has been evicted" do
|
315
|
+
it "should call the block and store and return the result" do
|
316
|
+
c = LRUCache.new
|
317
|
+
c[:a] = 'a'
|
318
|
+
c.send(:evict_lru!)
|
319
|
+
ttl = double(:ttl)
|
320
|
+
result = double(:result)
|
321
|
+
c.should_receive(:store).with(:a, result, ttl)
|
322
|
+
c.fetch(:a, ttl){ result }.should == result
|
323
|
+
end
|
324
|
+
end
|
325
|
+
context "when the key has expired" do
|
326
|
+
it "should call the block and store and return the result" do
|
327
|
+
c = LRUCache.new
|
328
|
+
now = Time.now
|
329
|
+
Timecop.freeze(now) { c.store(:a, 'a', now + 10) }
|
330
|
+
Timecop.freeze(now + 20) do
|
331
|
+
ttl = double(:ttl)
|
332
|
+
result = double(:result)
|
333
|
+
c.should_receive(:store).with(:a, result, ttl)
|
334
|
+
c.fetch(:a, ttl){ result }.should == result
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
context "when the key is present and un-expired" do
|
339
|
+
it "should return the cached value without calling the block" do
|
340
|
+
c = LRUCache.new(:ttl => nil)
|
341
|
+
c[:a] = 'a'
|
342
|
+
c.fetch(:a) { raise 'fail' }.should == 'a'
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
78
346
|
end
|
79
347
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
348
|
+
describe ".empty?" do
|
349
|
+
it "should return true if and only if size is zero" do
|
350
|
+
c = LRUCache.new
|
351
|
+
c.empty?.should be_true
|
352
|
+
c[:a] = 'a'
|
353
|
+
c.empty?.should be_false
|
354
|
+
c.clear
|
355
|
+
c.empty?.should be_true
|
356
|
+
end
|
87
357
|
end
|
88
358
|
|
89
|
-
describe ".
|
90
|
-
it "
|
91
|
-
c = LRUCache.new(:max_size =>
|
359
|
+
describe ".size and .keys" do
|
360
|
+
it "should return a count / list of entries in the cache" do
|
361
|
+
c = LRUCache.new(:max_size => 10)
|
362
|
+
(1..10).each do |i|
|
363
|
+
c.size.should == i-1
|
364
|
+
c[i] = :x
|
365
|
+
c.size.should == i
|
366
|
+
c.keys.sort.should == (1..i).to_a
|
367
|
+
end
|
368
|
+
(1..3).each do |i|
|
369
|
+
c.delete(i)
|
370
|
+
c.size.should == 10 - i
|
371
|
+
c.keys.sort.should == ((i+1)..10).to_a
|
372
|
+
end
|
373
|
+
(1..3).each do |i|
|
374
|
+
c[i] = :x
|
375
|
+
c.size.should == 7+i
|
376
|
+
c.keys.sort.should == (1..i).to_a + (4..10).to_a
|
377
|
+
end
|
378
|
+
(1..10).each do |i|
|
379
|
+
c[i] = :x
|
380
|
+
c.size.should == 10
|
381
|
+
c.keys.sort.should == (1..10).to_a
|
382
|
+
end
|
383
|
+
c.clear
|
384
|
+
c.size.should == 0
|
385
|
+
c.keys.should == []
|
386
|
+
end
|
387
|
+
it "may include expired entries" do
|
388
|
+
c = LRUCache.new(:ttl => 10, :max_size => 100)
|
389
|
+
now = Time.now
|
390
|
+
(0..19).each do |i|
|
391
|
+
Timecop.freeze(now + i) do
|
392
|
+
c[i] = :x
|
393
|
+
c.size.should == i+1
|
394
|
+
c.keys.sort.should == (0..i).to_a
|
395
|
+
end
|
396
|
+
end
|
397
|
+
Timecop.freeze(now + 20) do
|
398
|
+
c.size.should == 20
|
399
|
+
(0..19).each do |i|
|
400
|
+
c.include?(i)
|
401
|
+
end
|
402
|
+
c.size.should == 9
|
403
|
+
c.keys.sort.should == (11..19).to_a
|
404
|
+
end
|
405
|
+
end
|
406
|
+
it "should always return a value less than or equal to the cache's max_size" do
|
407
|
+
c = LRUCache.new(:max_size => 7)
|
408
|
+
(1..100).each do |i|
|
409
|
+
c[i] = :x
|
410
|
+
c.size.should <= 7
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
|
416
|
+
describe ".delete(key)" do
|
417
|
+
it "should remove the key from the internal hash and priority queue" do
|
418
|
+
c = LRUCache.new
|
419
|
+
c[:a] = 'a'
|
420
|
+
c.instance_variable_get(:@data).should include(:a)
|
421
|
+
c.instance_variable_get(:@pqueue).should include([:a,1])
|
422
|
+
c.delete(:a)
|
423
|
+
c.instance_variable_get(:@data).should_not include(:a)
|
424
|
+
c.instance_variable_get(:@pqueue).should_not include([:a,1])
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
describe ".evict_lru!" do
|
429
|
+
it "should find the least-recently used entry, and delete it" do
|
430
|
+
c = LRUCache.new
|
92
431
|
c[1] = 'a'
|
93
432
|
c[2] = 'b'
|
94
433
|
c[3] = 'c'
|
95
|
-
c.include?(1).should be_true
|
96
434
|
c[4] = 'd'
|
97
|
-
c[
|
98
|
-
c.
|
99
|
-
c.
|
435
|
+
c[:dne1].should be_nil # Doesn't affect LRU.
|
436
|
+
c[2].should == 'b'
|
437
|
+
c[1].should == 'a'
|
438
|
+
c[3].should == 'c'
|
439
|
+
c[4].should == 'd'
|
440
|
+
c[:dne2].should be_nil # Doesn't affect LRU.
|
441
|
+
c.keys.sort.should == [1,2,3,4]
|
442
|
+
c.send(:evict_lru!)
|
443
|
+
c.keys.sort.should == [1,3,4]
|
444
|
+
c.send(:evict_lru!)
|
445
|
+
c.keys.sort.should == [3,4]
|
446
|
+
c.send(:evict_lru!)
|
447
|
+
c.keys.sort.should == [4]
|
448
|
+
c.send(:evict_lru!)
|
449
|
+
c.keys.should == []
|
450
|
+
c.send(:evict_lru!)
|
451
|
+
c.keys.should == []
|
452
|
+
end
|
453
|
+
it "should leave the internal hash the same size as the priority queue" do
|
454
|
+
c = LRUCache.new
|
455
|
+
expected_size = 0
|
456
|
+
counter = 0
|
457
|
+
100.times do
|
458
|
+
if rand < 0.8
|
459
|
+
c[counter += 1] = :x
|
460
|
+
expected_size += 1
|
461
|
+
else
|
462
|
+
c.send(:evict_lru!)
|
463
|
+
expected_size -= 1
|
464
|
+
end
|
465
|
+
expected_size = [expected_size,0].max
|
466
|
+
c.instance_variable_get(:@data).size.should == expected_size
|
467
|
+
c.instance_variable_get(:@pqueue).count.should == expected_size
|
468
|
+
end
|
100
469
|
end
|
101
470
|
end
|
102
471
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lrucache
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 27
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
- 0
|
9
8
|
- 1
|
10
|
-
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Chris Johnson
|