record-cache 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|