record-cache 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/record-cache.rb +1 -0
- data/lib/record_cache/active_record.rb +318 -0
- data/lib/record_cache/base.rb +136 -0
- data/lib/record_cache/dispatcher.rb +90 -0
- data/lib/record_cache/multi_read.rb +51 -0
- data/lib/record_cache/query.rb +85 -0
- data/lib/record_cache/statistics.rb +82 -0
- data/lib/record_cache/strategy/base.rb +154 -0
- data/lib/record_cache/strategy/id_cache.rb +93 -0
- data/lib/record_cache/strategy/index_cache.rb +122 -0
- data/lib/record_cache/strategy/request_cache.rb +49 -0
- data/lib/record_cache/test/resettable_version_store.rb +49 -0
- data/lib/record_cache/version.rb +5 -0
- data/lib/record_cache/version_store.rb +54 -0
- data/lib/record_cache.rb +11 -0
- data/spec/db/database.yml +6 -0
- data/spec/db/schema.rb +42 -0
- data/spec/db/seeds.rb +40 -0
- data/spec/initializers/record_cache.rb +14 -0
- data/spec/lib/dispatcher_spec.rb +86 -0
- data/spec/lib/multi_read_spec.rb +51 -0
- data/spec/lib/query_spec.rb +148 -0
- data/spec/lib/statistics_spec.rb +140 -0
- data/spec/lib/strategy/base_spec.rb +241 -0
- data/spec/lib/strategy/id_cache_spec.rb +168 -0
- data/spec/lib/strategy/index_cache_spec.rb +223 -0
- data/spec/lib/strategy/request_cache_spec.rb +85 -0
- data/spec/lib/version_store_spec.rb +104 -0
- data/spec/models/apple.rb +8 -0
- data/spec/models/banana.rb +8 -0
- data/spec/models/pear.rb +6 -0
- data/spec/models/person.rb +11 -0
- data/spec/models/store.rb +13 -0
- data/spec/spec_helper.rb +44 -0
- data/spec/support/after_commit.rb +71 -0
- data/spec/support/matchers/hit_cache_matcher.rb +53 -0
- data/spec/support/matchers/miss_cache_matcher.rb +53 -0
- data/spec/support/matchers/use_cache_matcher.rb +53 -0
- metadata +253 -0
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RecordCache::Strategy::RequestCache do
|
4
|
+
|
5
|
+
it "should retrieve a record from the Request Cache" do
|
6
|
+
lambda{ Store.find(1) }.should miss_cache(Store)
|
7
|
+
lambda{ Store.find(1) }.should hit_cache(Store).on(:request_cache).times(1)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should retrieve the same record when the same query is used" do
|
11
|
+
@store_1 = Store.find(1)
|
12
|
+
@store_2 = Store.find(1)
|
13
|
+
@store_1.should == @store_2
|
14
|
+
@store_1.object_id.should == @store_2.object_id
|
15
|
+
end
|
16
|
+
|
17
|
+
context "logging" do
|
18
|
+
before(:each) do
|
19
|
+
Store.find(1)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should write hit to the debug log" do
|
23
|
+
mock(RecordCache::Base.logger).debug(/RequestCache hit for id=1\.L1|^(?!RequestCache)/).times(any_times)
|
24
|
+
Store.find(1)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should write miss to the debug log" do
|
28
|
+
mock(RecordCache::Base.logger).debug(/^RequestCache miss for id=2.L1|^(?!RequestCache)/).times(any_times)
|
29
|
+
Store.find(2)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "record_change" do
|
34
|
+
before(:each) do
|
35
|
+
# cache query in request cache
|
36
|
+
@store1 = Store.find(1)
|
37
|
+
@store2 = Store.find(2)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should remove all records from the cache for a specific model when one record is destroyed" do
|
41
|
+
lambda{ Store.find(1) }.should hit_cache(Store).on(:request_cache).times(1)
|
42
|
+
lambda{ Store.find(2) }.should hit_cache(Store).on(:request_cache).times(1)
|
43
|
+
@store1.destroy
|
44
|
+
lambda{ Store.find(2) }.should miss_cache(Store).on(:request_cache).times(1)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should remove all records from the cache for a specific model when one record is updated" do
|
48
|
+
lambda{ Store.find(1) }.should hit_cache(Store).on(:request_cache).times(1)
|
49
|
+
lambda{ Store.find(2) }.should hit_cache(Store).on(:request_cache).times(1)
|
50
|
+
@store1.name = "Store E"
|
51
|
+
@store1.save!
|
52
|
+
lambda{ Store.find(1) }.should miss_cache(Store).on(:request_cache).times(1)
|
53
|
+
lambda{ Store.find(2) }.should miss_cache(Store).on(:request_cache).times(1)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should remove all records from the cache for a specific model when one record is created" do
|
57
|
+
lambda{ Store.find(1) }.should hit_cache(Store).on(:request_cache).times(1)
|
58
|
+
lambda{ Store.find(2) }.should hit_cache(Store).on(:request_cache).times(1)
|
59
|
+
Store.create!(:name => "New Apple Store")
|
60
|
+
lambda{ Store.find(1) }.should miss_cache(Store).on(:request_cache).times(1)
|
61
|
+
lambda{ Store.find(2) }.should miss_cache(Store).on(:request_cache).times(1)
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
context "invalidate" do
|
67
|
+
before(:each) do
|
68
|
+
# cache query in request cache
|
69
|
+
@store1 = Store.find(1)
|
70
|
+
@store2 = Store.find(2)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should remove all records from the cache when clear is explicitly called" do
|
74
|
+
lambda{ Store.find(1) }.should hit_cache(Store).on(:request_cache).times(1)
|
75
|
+
RecordCache::Strategy::RequestCache.clear
|
76
|
+
lambda{ Store.find(1) }.should miss_cache(Store).on(:request_cache).times(1)
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should remove all records from the cache when invalidate is called" do
|
80
|
+
lambda{ Store.find(1) }.should hit_cache(Store).on(:request_cache).times(1)
|
81
|
+
Store.record_cache.invalidate(:request_cache, @store2)
|
82
|
+
lambda{ Store.find(1) }.should miss_cache(Store).on(:request_cache).times(1)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RecordCache::VersionStore do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@version_store = RecordCache::Base.version_store
|
7
|
+
@version_store.store.write("key1", 1000)
|
8
|
+
@version_store.store.write("key2", 2000)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should only accept ActiveSupport cache stores" do
|
12
|
+
lambda { RecordCache::VersionStore.new(Object.new) }.should raise_error("Must be an ActiveSupport::Cache::Store")
|
13
|
+
end
|
14
|
+
|
15
|
+
context "current" do
|
16
|
+
it "should retrieve the current version" do
|
17
|
+
@version_store.current("key1").should == 1000
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should retrieve nil for unknown keys" do
|
21
|
+
@version_store.current("unknown_key").should == nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "current_multi" do
|
26
|
+
it "should retrieve all versions" do
|
27
|
+
@version_store.current_multi({:id1 => "key1", :id2 => "key2"}).should == {:id1 => 1000, :id2 => 2000}
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should return nil for unknown keys" do
|
31
|
+
@version_store.current_multi({:id1 => "key1", :key3 => "unknown_key"}).should == {:id1 => 1000, :key3 => nil}
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should use read_multi on the underlying store" do
|
35
|
+
mock(@version_store.store).read_multi(/key[12]/, /key[12]/) { {"key1" => 5, "key2" => 6} }
|
36
|
+
@version_store.current_multi({:id1 => "key1", :id2 => "key2"}).should == {:id1 => 5, :id2 => 6}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "renew" do
|
41
|
+
it "should renew the version" do
|
42
|
+
@version_store.current("key1").should == 1000
|
43
|
+
@version_store.renew("key1")
|
44
|
+
@version_store.current("key1").should_not == 1000
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should renew the version for unknown keys" do
|
48
|
+
@version_store.current("unknown_key").should == nil
|
49
|
+
@version_store.renew("unknown_key")
|
50
|
+
@version_store.current("unknown_key").should_not == nil
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should write to the debug log" do
|
54
|
+
mock(RecordCache::Base.logger).debug?{ true }
|
55
|
+
mock(RecordCache::Base.logger).debug(/Version Store: renew key1: nil => \d+/)
|
56
|
+
@version_store.renew("key1")
|
57
|
+
stub(RecordCache::Base.logger).debug?{ false } # to prevent the ResettableVersionStore from logging in +after(:each)+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context "increment" do
|
62
|
+
it "should increment the version" do
|
63
|
+
@version_store.current("key1").should == 1000
|
64
|
+
@version_store.increment("key1")
|
65
|
+
@version_store.current("key1").should == 1001
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should renew the version on increment for unknown keys" do
|
69
|
+
# do not use unknown_key as the version store retains the value after this spec
|
70
|
+
@version_store.current("unknown_key").should == nil
|
71
|
+
@version_store.renew("unknown_key")
|
72
|
+
@version_store.current("unknown_key").should_not == nil
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should write to the debug log" do
|
76
|
+
mock(RecordCache::Base.logger).debug?{ true }
|
77
|
+
mock(RecordCache::Base.logger).debug("Version Store: incremented key1: 1000 => 1001")
|
78
|
+
@version_store.increment("key1")
|
79
|
+
stub(RecordCache::Base.logger).debug?{ false } # to prevent the ResettableVersionStore from logging in +after(:each)+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context "delete" do
|
84
|
+
it "should delete the version" do
|
85
|
+
@version_store.current("key1").should == 1000
|
86
|
+
@version_store.delete("key1").should == true
|
87
|
+
@version_store.current("key1").should == nil
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should not raise an error when deleting the version for unknown keys" do
|
91
|
+
@version_store.current("unknown_key").should == nil
|
92
|
+
@version_store.delete("unknown_key").should == false
|
93
|
+
@version_store.current("unknown_key").should == nil
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should write to the debug log" do
|
97
|
+
mock(RecordCache::Base.logger).debug?{ true }
|
98
|
+
mock(RecordCache::Base.logger).debug("Version Store: deleted key1")
|
99
|
+
@version_store.delete("key1")
|
100
|
+
stub(RecordCache::Base.logger).debug?{ false } # to prevent the ResettableVersionStore from logging in +after(:each)+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
data/spec/models/pear.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
class Person < ActiveRecord::Base
|
2
|
+
|
3
|
+
cache_records :store => :shared, :key => "per"
|
4
|
+
|
5
|
+
has_many :apples # cached with index on person_id
|
6
|
+
has_many :bananas # cached with index on person_id
|
7
|
+
has_many :pears # not cached
|
8
|
+
|
9
|
+
has_and_belongs_to_many :stores
|
10
|
+
|
11
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class Store < ActiveRecord::Base
|
2
|
+
|
3
|
+
cache_records :store => :local, :key => "st", :request_cache => true
|
4
|
+
|
5
|
+
belongs_to :owner, :class_name => "Person"
|
6
|
+
|
7
|
+
has_many :apples, :autosave => true # cached with index on store
|
8
|
+
has_many :bananas # cached without index on store
|
9
|
+
has_many :pears # not cached
|
10
|
+
|
11
|
+
has_and_belongs_to_many :customers, :class_name => "Person"
|
12
|
+
|
13
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
dir = File.dirname(__FILE__)
|
2
|
+
$LOAD_PATH.unshift dir + "/../lib"
|
3
|
+
$LOAD_PATH.unshift dir
|
4
|
+
|
5
|
+
require "rubygems"
|
6
|
+
require "test/unit"
|
7
|
+
require "rspec"
|
8
|
+
require 'rr'
|
9
|
+
require 'database_cleaner'
|
10
|
+
require "logger"
|
11
|
+
require "record_cache"
|
12
|
+
require "record_cache/test/resettable_version_store"
|
13
|
+
|
14
|
+
# spec support files
|
15
|
+
Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f}
|
16
|
+
|
17
|
+
# logging
|
18
|
+
Dir.mkdir(dir + "/log") unless File.exists?(dir + "/log")
|
19
|
+
ActiveRecord::Base.logger = Logger.new(dir + "/log/debug.log")
|
20
|
+
# ActiveRecord::Base.logger = Logger.new(STDOUT)
|
21
|
+
|
22
|
+
# SQL Lite
|
23
|
+
ActiveRecord::Base.configurations = YAML::load(IO.read(dir + "/db/database.yml"))
|
24
|
+
ActiveRecord::Base.establish_connection("sqlite3")
|
25
|
+
|
26
|
+
# Initializers + Model + Data
|
27
|
+
load(dir + "/initializers/record_cache.rb")
|
28
|
+
load(dir + "/db/schema.rb")
|
29
|
+
Dir["#{dir}/models/*.rb"].each {|f| load(f) }
|
30
|
+
load(dir + "/db/seeds.rb")
|
31
|
+
|
32
|
+
# Clear cache after each test
|
33
|
+
RSpec.configure do |config|
|
34
|
+
config.mock_with :rr
|
35
|
+
|
36
|
+
config.before(:each) do
|
37
|
+
DatabaseCleaner.start
|
38
|
+
end
|
39
|
+
|
40
|
+
config.after(:each) do
|
41
|
+
DatabaseCleaner.clean
|
42
|
+
RecordCache::Base.version_store.reset!
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# @see http://outofti.me/post/4777884779/test-after-commit-hooks-with-transactional-fixtures
|
2
|
+
module ActiveRecord
|
3
|
+
module ConnectionAdapters
|
4
|
+
module DatabaseStatements
|
5
|
+
#
|
6
|
+
# Run the normal transaction method; when it's done, check to see if there
|
7
|
+
# is exactly one open transaction. If so, that's the transactional
|
8
|
+
# fixtures transaction; from the model's standpoint, the completed
|
9
|
+
# transaction is the real deal. Send commit callbacks to models.
|
10
|
+
#
|
11
|
+
# If the transaction block raises a Rollback, we need to know, so we don't
|
12
|
+
# call the commit hooks. Other exceptions don't need to be explicitly
|
13
|
+
# accounted for since they will raise uncaught through this method and
|
14
|
+
# prevent the code after the hook from running.
|
15
|
+
#
|
16
|
+
def transaction_with_transactional_fixtures(options = {}, &block)
|
17
|
+
rolled_back = false
|
18
|
+
|
19
|
+
transaction_without_transactional_fixtures do
|
20
|
+
begin
|
21
|
+
yield
|
22
|
+
rescue Exception => exception
|
23
|
+
if exception.is_a?(ActiveRecord::Rollback)
|
24
|
+
rolled_back = true
|
25
|
+
else
|
26
|
+
puts "Exception in aftercommit: #{exception}"
|
27
|
+
puts exception.backtrace
|
28
|
+
end
|
29
|
+
raise exception
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
if !rolled_back && open_transactions == 1
|
34
|
+
commit_transaction_records(false)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
alias_method_chain :transaction, :transactional_fixtures
|
38
|
+
|
39
|
+
#
|
40
|
+
# The @_current_transaction_records is an stack of arrays, each one
|
41
|
+
# containing the records associated with the corresponding transaction
|
42
|
+
# in the transaction stack. This is used by the
|
43
|
+
# `rollback_transaction_records` method (to only send a rollback hook to
|
44
|
+
# models attached to the transaction being rolled back) but is usually
|
45
|
+
# ignored by the `commit_transaction_records` method. Here we
|
46
|
+
# monkey-patch it to temporarily replace the array with only the records
|
47
|
+
# for the top-of-stack transaction, so the real
|
48
|
+
# `commit_transaction_records` method only sends callbacks to those.
|
49
|
+
#
|
50
|
+
def commit_transaction_records_with_transactional_fixtures(commit = true)
|
51
|
+
unless commit
|
52
|
+
real_current_transaction_records = @_current_transaction_records
|
53
|
+
@_current_transaction_records = @_current_transaction_records.pop
|
54
|
+
end
|
55
|
+
|
56
|
+
begin
|
57
|
+
@_current_transaction_records ||= []
|
58
|
+
commit_transaction_records_without_transactional_fixtures
|
59
|
+
rescue Exception => exception
|
60
|
+
puts "Error in aftercommit: #{exception}"
|
61
|
+
puts exception.backtrace
|
62
|
+
ensure
|
63
|
+
unless commit
|
64
|
+
@_current_transaction_records = real_current_transaction_records
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
alias_method_chain :commit_transaction_records, :transactional_fixtures
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# Examples:
|
2
|
+
# 1) lambda{ Person.find(22) }.should hit_cache(Person)
|
3
|
+
# _should have at least one hit in any of the cache strategies for the Person model_
|
4
|
+
#
|
5
|
+
# 2) lambda{ Person.find(22) }.should hit_cache(Person).on(:id)
|
6
|
+
# _should have at least one hit in the ID cache strategy for the Person model_
|
7
|
+
#
|
8
|
+
# 3) lambda{ Person.find_by_ids(22, 23, 24) }.should hit_cache(Person).on(:id).times(2)
|
9
|
+
# _should have exactly two hits in the :id cache strategy for the Person model_
|
10
|
+
#
|
11
|
+
# 4) lambda{ Person.find_by_ids(22, 23, 24) }.should hit_cache(Person).times(3)
|
12
|
+
# _should have exactly three hits in any of the cache strategies for the Person model_
|
13
|
+
RSpec::Matchers.define :hit_cache do |model|
|
14
|
+
|
15
|
+
chain :on do |strategy|
|
16
|
+
@strategy = strategy
|
17
|
+
end
|
18
|
+
|
19
|
+
chain :times do |nr_of_hits|
|
20
|
+
@expected_nr_of_hits = nr_of_hits
|
21
|
+
end
|
22
|
+
|
23
|
+
match do |proc|
|
24
|
+
# reset statistics for the given model and start counting
|
25
|
+
RecordCache::Statistics.reset!(model)
|
26
|
+
RecordCache::Statistics.start
|
27
|
+
# call the given proc
|
28
|
+
proc.call
|
29
|
+
# collect statistics for the model
|
30
|
+
@stats = RecordCache::Statistics.find(model)
|
31
|
+
# check the nr of hits
|
32
|
+
@nr_of_hits = @strategy ? @stats[@strategy].hits : @stats.values.map{ |s| s.hits }.sum
|
33
|
+
# test nr of hits
|
34
|
+
@expected_nr_of_hits ? @nr_of_hits == @expected_nr_of_hits : @nr_of_hits > 0
|
35
|
+
end
|
36
|
+
|
37
|
+
failure_message_for_should do |proc|
|
38
|
+
prepare_message
|
39
|
+
"Expected #{@strategy_msg} for #{model.name} to be hit #{@times_msg}, but found #{@nr_of_hits}: #{@statistics_msg}"
|
40
|
+
end
|
41
|
+
|
42
|
+
failure_message_for_should_not do |proc|
|
43
|
+
prepare_message
|
44
|
+
"Expected #{@strategy_msg} for #{model.name} not to be hit #{@times_msg}, but found #{@nr_of_hits}: #{@statistics_msg}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def prepare_message
|
48
|
+
@strategy_msg = @strategy ? "the #{@strategy} cache" : "any of the caches"
|
49
|
+
@times_msg = @expected_nr_of_hits ? (@expected_nr_of_hits == 1 ? "exactly once" : "exactly #{@expected_nr_of_hits} times") : "at least once"
|
50
|
+
@statistics_msg = @stats.map{|strategy, s| "#{strategy} => #{s.inspect}" }.join(", ")
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# Examples:
|
2
|
+
# 1) lambda{ Person.find(22) }.should miss_cache(Person)
|
3
|
+
# _should have at least one miss in any of the cache strategies for the Person model_
|
4
|
+
#
|
5
|
+
# 2) lambda{ Person.find(22) }.should miss_cache(Person).on(:id)
|
6
|
+
# _should have at least one miss for the ID cache strategy for the Person model_
|
7
|
+
#
|
8
|
+
# 3) lambda{ Person.find_by_ids(22, 23, 24) }.should miss_cache(Person).on(:id).times(2)
|
9
|
+
# _should have exactly two misses in the :id cache strategy for the Person model_
|
10
|
+
#
|
11
|
+
# 4) lambda{ Person.find_by_ids(22, 23, 24) }.should miss_cache(Person).times(3)
|
12
|
+
# _should have exactly three misses in any of the cache strategies for the Person model_
|
13
|
+
RSpec::Matchers.define :miss_cache do |model|
|
14
|
+
|
15
|
+
chain :on do |strategy|
|
16
|
+
@strategy = strategy
|
17
|
+
end
|
18
|
+
|
19
|
+
chain :times do |nr_of_misses|
|
20
|
+
@expected_nr_of_misses = nr_of_misses
|
21
|
+
end
|
22
|
+
|
23
|
+
match do |proc|
|
24
|
+
# reset statistics for the given model and start counting
|
25
|
+
RecordCache::Statistics.reset!(model)
|
26
|
+
RecordCache::Statistics.start
|
27
|
+
# call the given proc
|
28
|
+
proc.call
|
29
|
+
# collect statistics for the model
|
30
|
+
@stats = RecordCache::Statistics.find(model)
|
31
|
+
# check the nr of misses
|
32
|
+
@nr_of_misses = @strategy ? @stats[@strategy].misses : @stats.values.map{ |s| s.misses }.sum
|
33
|
+
# test nr of misses
|
34
|
+
@expected_nr_of_misses ? @nr_of_misses == @expected_nr_of_misses : @nr_of_misses > 0
|
35
|
+
end
|
36
|
+
|
37
|
+
failure_message_for_should do |proc|
|
38
|
+
prepare_message
|
39
|
+
"Expected #{@strategy_msg} for #{model.name} to be missed #{@times_msg}, but found #{@nr_of_misses}: #{@statistics_msg}"
|
40
|
+
end
|
41
|
+
|
42
|
+
failure_message_for_should_not do |proc|
|
43
|
+
prepare_message
|
44
|
+
"Expected #{@strategy_msg} for #{model.name} not to be missed #{@times_msg}, but found #{@nr_of_misses}: #{@statistics_msg}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def prepare_message
|
48
|
+
@strategy_msg = @strategy ? "the #{@strategy} cache" : "any of the caches"
|
49
|
+
@times_msg = @expected_nr_of_misses ? (@expected_nr_of_misses == 1 ? "exactly once" : "exactly #{@expected_nr_of_misses} times") : "at least once"
|
50
|
+
@statistics_msg = @stats.map{|strategy, s| "#{strategy} => #{s.inspect}" }.join(", ")
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# Examples:
|
2
|
+
# 1) lambda{ Person.find(22) }.should use_cache(Person)
|
3
|
+
# _should perform at least one call (hit/miss) to any of the cache strategies for the Person model_
|
4
|
+
#
|
5
|
+
# 2) lambda{ Person.find(22) }.should use_cache(Person).on(:id)
|
6
|
+
# _should perform at least one call (hit/miss) to the ID cache strategy for the Person model_
|
7
|
+
#
|
8
|
+
# 3) lambda{ Person.find_by_ids(22, 23, 24) }.should use_cache(Person).on(:id).times(2)
|
9
|
+
# _should perform exactly two calls (hit/miss) to the :id cache strategy for the Person model_
|
10
|
+
#
|
11
|
+
# 4) lambda{ Person.find_by_ids(22, 23, 24) }.should use_cache(Person).times(3)
|
12
|
+
# _should perform exactly three calls (hit/miss) to any of the cache strategies for the Person model_
|
13
|
+
RSpec::Matchers.define :use_cache do |model|
|
14
|
+
|
15
|
+
chain :on do |strategy|
|
16
|
+
@strategy = strategy
|
17
|
+
end
|
18
|
+
|
19
|
+
chain :times do |nr_of_calls|
|
20
|
+
@expected_nr_of_calls = nr_of_calls
|
21
|
+
end
|
22
|
+
|
23
|
+
match do |proc|
|
24
|
+
# reset statistics for the given model and start counting
|
25
|
+
RecordCache::Statistics.reset!(model)
|
26
|
+
RecordCache::Statistics.start
|
27
|
+
# call the given proc
|
28
|
+
proc.call
|
29
|
+
# collect statistics for the model
|
30
|
+
@stats = RecordCache::Statistics.find(model)
|
31
|
+
# check the nr of calls
|
32
|
+
@nr_of_calls = @strategy ? @stats[@strategy].calls : @stats.values.map{ |s| s.calls }.sum
|
33
|
+
# test nr of calls
|
34
|
+
@expected_nr_of_calls ? @nr_of_calls == @expected_nr_of_calls : @nr_of_calls > 0
|
35
|
+
end
|
36
|
+
|
37
|
+
failure_message_for_should do |proc|
|
38
|
+
prepare_message
|
39
|
+
"Expected #{@strategy_msg} for #{model.name} to be called #{@times_msg}, but found #{@nr_of_calls}: #{@statistics_msg}"
|
40
|
+
end
|
41
|
+
|
42
|
+
failure_message_for_should_not do |proc|
|
43
|
+
prepare_message
|
44
|
+
"Expected #{@strategy_msg} for #{model.name} not to be called #{@times_msg}, but found #{@nr_of_calls}: #{@statistics_msg}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def prepare_message
|
48
|
+
@strategy_msg = @strategy ? "the #{@strategy} cache" : "any of the caches"
|
49
|
+
@times_msg = @expected_nr_of_calls ? (@expected_nr_of_calls == 1 ? "exactly once" : "exactly #{@expected_nr_of_calls} times") : "at least once"
|
50
|
+
@statistics_msg = @stats.map{|strategy, s| "#{strategy} => #{s.inspect}" }.join(", ")
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|