Repository: freerange/mocha (Ruby)

Issue:
Change in partial expectation evaluation time?
It appears that in 1.10.0, the time at which partial expectations start recording changed, but I don't see anything in the release notes that explains what I'm seeing. I suspect that <1.10.0, they started recording at the time of partial definition, but >=1.10.0 they start recording only when they've been finalized with a quantifier.  For example, see this simplified snippet of some code found in the wild:

```
before do
  exp = Foo.any_instance.expects(:action)
end

it 'invokes' do
  Foo.action
  exp.twice
  Foo.action
end
```

Moving the `twice` up a line works regardless of version.

Is this expected? Is there another explanation?


Discussion:
@srvance Thanks opening this issue. I'm on holiday at the moment, so it might be a while before I can investigate. We have been doing a bunch of internal refactoring, so that might explain the change in behaviour, although we worked pretty hard to avoid any such change.

@nitishr I don't suppose you have time to investigate this...?
No problem. I just wanted to bring it to your attention since it didn't seem documented. I can work around it by reordering the statements. This is part of a big Rails upgrade project.

Historically, it appears to have gotten this way by someone mechanically moving the first `Foo.action` from a `before` to the `it` as needs changed within the `describe`.
Hi @srvance. Thank you for reporting. I've just tried out the scenario you've described under v1.9.0 and master, and noticed the difference. I'll investigate this.

Could you please describe the impact this is having on you - it'll help me understand both the need for a 'fix' and the urgency for it.
No urgency.

It took awhile to figure out why a few tests were suddenly failing. The mocha update was incidental to updating other gems, but it was a big jump (1.2.1 -> 1.11.2).

I'd just vote for a troubleshooting documentation item or perhaps a deprecation warning if the situation can be recognized. Once I figured it out, it's easy to fix. As you can imagine, in a more complex context, there's also archeology involved in determining whether the test was written intentionally and so forth. The biggest opportunity is just saving others possibly a good bit of confusion and investigative time.
OK, I've nailed the issue down to 59454a83648f76e46918779f7dd39a34927cff2c. When you qualify the expectation with a (different) quantifier, the `Cardinality` gets changed, and with it, the prior invocations it had recorded get lost/forgotten.

I'll work on a fix for this tomorrow.
Interesting. So in this case, there was no explicit quantifier, which counts as a default quantifier that gets discarded, resetting the recordings, when the explicit quantifier is added?
@srvance Yes, you've got it right.

@floehopper I've issued #474 to fix this regression.



Patch:
diff --git a/lib/mocha/cardinality.rb b/lib/mocha/cardinality.rb
index 703e0535c..ab9b4f38e 100644
--- a/lib/mocha/cardinality.rb
+++ b/lib/mocha/cardinality.rb
@@ -2,31 +2,28 @@ module Mocha
   class Cardinality
     INFINITY = 1 / 0.0
 
-    class << self
-      def exactly(count)
-        new(count, count)
-      end
+    def initialize(required = 0, maximum = INFINITY)
+      update(required, maximum)
+      @invocations = []
+    end
 
-      def at_least(count)
-        new(count, INFINITY)
-      end
+    def exactly(count)
+      update(count, count)
+    end
 
-      def at_most(count)
-        new(0, count)
-      end
+    def at_least(count)
+      update(count, INFINITY)
+    end
 
-      def times(range_or_count)
-        case range_or_count
-        when Range then new(range_or_count.first, range_or_count.last)
-        else new(range_or_count, range_or_count)
-        end
-      end
+    def at_most(count)
+      update(0, count)
     end
 
-    def initialize(required, maximum)
-      @required = required
-      @maximum = maximum
-      @invocations = []
+    def times(range_or_count)
+      case range_or_count
+      when Range then update(range_or_count.first, range_or_count.last)
+      else update(range_or_count, range_or_count)
+      end
     end
 
     def <<(invocation)
@@ -62,21 +59,21 @@ def anticipated_times
       if allowed_any_number_of_times?
         'allowed any number of times'
       elsif required.zero? && maximum.zero?
-        "expected #{times(maximum)}"
+        "expected #{count(maximum)}"
       elsif required == maximum
-        "expected exactly #{times(required)}"
+        "expected exactly #{count(required)}"
       elsif infinite?(maximum)
-        "expected at least #{times(required)}"
+        "expected at least #{count(required)}"
       elsif required.zero?
-        "expected at most #{times(maximum)}"
+        "expected at most #{count(maximum)}"
       else
-        "expected between #{required} and #{times(maximum)}"
+        "expected between #{required} and #{count(maximum)}"
       end
     end
     # rubocop:enable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
 
     def invoked_times
-      "invoked #{times(@invocations.size)}"
+      "invoked #{count(@invocations.size)}"
     end
 
     def actual_invocations
@@ -87,7 +84,7 @@ def actual_invocations
 
     attr_reader :required, :maximum
 
-    def times(number)
+    def count(number)
       case number
       when 0 then 'never'
       when 1 then 'once'
@@ -96,6 +93,12 @@ def times(number)
       end
     end
 
+    def update(required, maximum)
+      @required = required
+      @maximum = maximum
+      self
+    end
+
     def infinite?(number)
       number.respond_to?(:infinite?) && number.infinite?
     end
diff --git a/lib/mocha/expectation.rb b/lib/mocha/expectation.rb
index 12b0f8ebe..447b59aad 100644
--- a/lib/mocha/expectation.rb
+++ b/lib/mocha/expectation.rb
@@ -42,7 +42,7 @@ class Expectation
     #   object.expected_method
     #   # => verify fails
     def times(range)
-      @cardinality = Cardinality.times(range)
+      @cardinality.times(range)
       self
     end
 
@@ -68,7 +68,7 @@ def times(range)
     #   object.expected_method
     #   # => verify fails
     def twice
-      @cardinality = Cardinality.exactly(2)
+      @cardinality.exactly(2)
       self
     end
 
@@ -93,7 +93,7 @@ def twice
     #   object.expects(:expected_method).once
     #   # => verify fails
     def once
-      @cardinality = Cardinality.exactly(1)
+      @cardinality.exactly(1)
       self
     end
 
@@ -110,7 +110,7 @@ def once
     #   object.expects(:expected_method).never
     #   # => verify succeeds
     def never
-      @cardinality = Cardinality.exactly(0)
+      @cardinality.exactly(0)
       self
     end
 
@@ -130,7 +130,7 @@ def never
     #   object.expected_method
     #   # => verify fails
     def at_least(minimum_number_of_times)
-      @cardinality = Cardinality.at_least(minimum_number_of_times)
+      @cardinality.at_least(minimum_number_of_times)
       self
     end
 
@@ -149,7 +149,6 @@ def at_least(minimum_number_of_times)
     #   # => verify fails
     def at_least_once
       at_least(1)
-      self
     end
 
     # Modifies expectation so that the expected method must be called at most a +maximum_number_of_times+.
@@ -167,7 +166,7 @@ def at_least_once
     #   object.expects(:expected_method).at_most(2)
     #   3.times { object.expected_method } # => unexpected invocation
     def at_most(maximum_number_of_times)
-      @cardinality = Cardinality.at_most(maximum_number_of_times)
+      @cardinality.at_most(maximum_number_of_times)
       self
     end
 
@@ -186,7 +185,6 @@ def at_most(maximum_number_of_times)
     #   2.times { object.expected_method } # => unexpected invocation
     def at_most_once
       at_most(1)
-      self
     end
 
     # Modifies expectation so that the expected method must be called with +expected_parameters+.
@@ -549,7 +547,7 @@ def initialize(mock, expected_method_name, backtrace = nil)
       @block_matcher = BlockMatchers::OptionalBlock.new
       @ordering_constraints = []
       @side_effects = []
-      @cardinality = Cardinality.exactly(1)
+      @cardinality = Cardinality.new.exactly(1)
       @return_values = ReturnValues.new
       @yield_parameters = YieldParameters.new
       @backtrace = backtrace || caller


Test patch:
diff --git a/test/acceptance/expected_invocation_count_test.rb b/test/acceptance/expected_invocation_count_test.rb
index 558015ca9..836c561cc 100644
--- a/test/acceptance/expected_invocation_count_test.rb
+++ b/test/acceptance/expected_invocation_count_test.rb
@@ -226,4 +226,15 @@ def test_should_fail_fast_if_there_is_no_matching_expectation
       '- expected exactly once, invoked never: #<Mock:mock>.method(1)'
     ], test_result.failure_message_lines
   end
+
+  def test_should_pass_if_cardinality_is_satisfied_using_calls_made_both_before_and_after_updating_cardinality
+    test_result = run_as_test do
+      mock = mock('mock')
+      expectation = mock.expects(:method)
+      mock.method
+      expectation.twice
+      mock.method
+    end
+    assert_passed(test_result)
+  end
 end
diff --git a/test/unit/cardinality_test.rb b/test/unit/cardinality_test.rb
index 885820abf..1c265e97a 100644
--- a/test/unit/cardinality_test.rb
+++ b/test/unit/cardinality_test.rb
@@ -34,39 +34,39 @@ def test_should_be_satisfied_if_invocations_so_far_have_reached_required_thresho
   end
 
   def test_should_describe_cardinality_defined_using_at_least
-    assert_equal 'allowed any number of times', Cardinality.at_least(0).anticipated_times
-    assert_equal 'expected at least once', Cardinality.at_least(1).anticipated_times
-    assert_equal 'expected at least twice', Cardinality.at_least(2).anticipated_times
-    assert_equal 'expected at least 3 times', Cardinality.at_least(3).anticipated_times
+    assert_equal 'allowed any number of times', Cardinality.new.at_least(0).anticipated_times
+    assert_equal 'expected at least once', Cardinality.new.at_least(1).anticipated_times
+    assert_equal 'expected at least twice', Cardinality.new.at_least(2).anticipated_times
+    assert_equal 'expected at least 3 times', Cardinality.new.at_least(3).anticipated_times
   end
 
   def test_should_describe_cardinality_defined_using_at_most
-    assert_equal 'expected at most once', Cardinality.at_most(1).anticipated_times
-    assert_equal 'expected at most twice', Cardinality.at_most(2).anticipated_times
-    assert_equal 'expected at most 3 times', Cardinality.at_most(3).anticipated_times
+    assert_equal 'expected at most once', Cardinality.new.at_most(1).anticipated_times
+    assert_equal 'expected at most twice', Cardinality.new.at_most(2).anticipated_times
+    assert_equal 'expected at most 3 times', Cardinality.new.at_most(3).anticipated_times
   end
 
   def test_should_describe_cardinality_defined_using_exactly
-    assert_equal 'expected never', Cardinality.exactly(0).anticipated_times
-    assert_equal 'expected exactly once', Cardinality.exactly(1).anticipated_times
-    assert_equal 'expected exactly twice', Cardinality.exactly(2).anticipated_times
-    assert_equal 'expected exactly 3 times', Cardinality.exactly(3).anticipated_times
+    assert_equal 'expected never', Cardinality.new.exactly(0).anticipated_times
+    assert_equal 'expected exactly once', Cardinality.new.exactly(1).anticipated_times
+    assert_equal 'expected exactly twice', Cardinality.new.exactly(2).anticipated_times
+    assert_equal 'expected exactly 3 times', Cardinality.new.exactly(3).anticipated_times
   end
 
   def test_should_describe_cardinality_defined_using_times_with_range
-    assert_equal 'expected between 2 and 4 times', Cardinality.times(2..4).anticipated_times
-    assert_equal 'expected between 1 and 3 times', Cardinality.times(1..3).anticipated_times
+    assert_equal 'expected between 2 and 4 times', Cardinality.new.times(2..4).anticipated_times
+    assert_equal 'expected between 1 and 3 times', Cardinality.new.times(1..3).anticipated_times
   end
 
   def test_should_need_verifying
-    assert Cardinality.exactly(2).needs_verifying?
-    assert Cardinality.at_least(3).needs_verifying?
-    assert Cardinality.at_most(2).needs_verifying?
-    assert Cardinality.times(4).needs_verifying?
-    assert Cardinality.times(2..4).needs_verifying?
+    assert Cardinality.new.exactly(2).needs_verifying?
+    assert Cardinality.new.at_least(3).needs_verifying?
+    assert Cardinality.new.at_most(2).needs_verifying?
+    assert Cardinality.new.times(4).needs_verifying?
+    assert Cardinality.new.times(2..4).needs_verifying?
   end
 
   def test_should_not_need_verifying
-    assert_equal false, Cardinality.at_least(0).needs_verifying?
+    assert_equal false, Cardinality.new.at_least(0).needs_verifying?
   end
 end