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.
Files changed (39) hide show
  1. data/lib/record-cache.rb +1 -0
  2. data/lib/record_cache/active_record.rb +318 -0
  3. data/lib/record_cache/base.rb +136 -0
  4. data/lib/record_cache/dispatcher.rb +90 -0
  5. data/lib/record_cache/multi_read.rb +51 -0
  6. data/lib/record_cache/query.rb +85 -0
  7. data/lib/record_cache/statistics.rb +82 -0
  8. data/lib/record_cache/strategy/base.rb +154 -0
  9. data/lib/record_cache/strategy/id_cache.rb +93 -0
  10. data/lib/record_cache/strategy/index_cache.rb +122 -0
  11. data/lib/record_cache/strategy/request_cache.rb +49 -0
  12. data/lib/record_cache/test/resettable_version_store.rb +49 -0
  13. data/lib/record_cache/version.rb +5 -0
  14. data/lib/record_cache/version_store.rb +54 -0
  15. data/lib/record_cache.rb +11 -0
  16. data/spec/db/database.yml +6 -0
  17. data/spec/db/schema.rb +42 -0
  18. data/spec/db/seeds.rb +40 -0
  19. data/spec/initializers/record_cache.rb +14 -0
  20. data/spec/lib/dispatcher_spec.rb +86 -0
  21. data/spec/lib/multi_read_spec.rb +51 -0
  22. data/spec/lib/query_spec.rb +148 -0
  23. data/spec/lib/statistics_spec.rb +140 -0
  24. data/spec/lib/strategy/base_spec.rb +241 -0
  25. data/spec/lib/strategy/id_cache_spec.rb +168 -0
  26. data/spec/lib/strategy/index_cache_spec.rb +223 -0
  27. data/spec/lib/strategy/request_cache_spec.rb +85 -0
  28. data/spec/lib/version_store_spec.rb +104 -0
  29. data/spec/models/apple.rb +8 -0
  30. data/spec/models/banana.rb +8 -0
  31. data/spec/models/pear.rb +6 -0
  32. data/spec/models/person.rb +11 -0
  33. data/spec/models/store.rb +13 -0
  34. data/spec/spec_helper.rb +44 -0
  35. data/spec/support/after_commit.rb +71 -0
  36. data/spec/support/matchers/hit_cache_matcher.rb +53 -0
  37. data/spec/support/matchers/miss_cache_matcher.rb +53 -0
  38. data/spec/support/matchers/use_cache_matcher.rb +53 -0
  39. 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
@@ -0,0 +1,8 @@
1
+ class Apple < ActiveRecord::Base
2
+
3
+ cache_records :store => :shared, :key => "apl", :index => [:store_id, :person_id]
4
+
5
+ belongs_to :store
6
+ belongs_to :person
7
+
8
+ end
@@ -0,0 +1,8 @@
1
+ class Banana < ActiveRecord::Base
2
+
3
+ cache_records :store => :local, :index => [:person_id]
4
+
5
+ belongs_to :store
6
+ belongs_to :person
7
+
8
+ end
@@ -0,0 +1,6 @@
1
+ class Pear < ActiveRecord::Base
2
+
3
+ belongs_to :store
4
+ belongs_to :person
5
+
6
+ end
@@ -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
@@ -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