Skip to content

Commit

Permalink
Fix invocation matching never expectation logic
Browse files Browse the repository at this point in the history
c.f. 01874f1

In #681 I added logic to display a deprecation warning about a change I
planned to make in this PR. However, it turns out the logic in both was
faulty as demonstrated in this example [1] where the deprecation warning
was incorrectly displayed for the 2nd call to `Foo.create_if`:

    class Foo
      def self.create_if(condition)
        new if condition
      end
    end

    test "it creates only if condition is true" do
      Foo.expects(:new).never
      Foo.create_if(false)

      Foo.expects(:new).once
      Foo.create_if(true)
    end

This commit adds a new acceptance test to cover an example where the
logic was incorrect and updates the logic in `Mock#handle_method_call`
to fix the logic so this new test passes and none of the existing tests
fail. Unfortunately the implementation is now rather complicated and
hard to follow, but I think it will do for now. I have a couple of ideas
for ways to simplify it, but I don't have time to work on that now.

[1]: #679 (comment)
  • Loading branch information
floehopper committed Nov 28, 2024
1 parent 15985bc commit 29a8e08
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 9 deletions.
23 changes: 14 additions & 9 deletions lib/mocha/mock.rb
Original file line number Diff line number Diff line change
Expand Up @@ -321,22 +321,27 @@ def method_missing(symbol, *arguments, &block) # rubocop:disable Style/MethodMis
ruby2_keywords(:method_missing)

# @private
def handle_method_call(symbol, arguments, block)
def handle_method_call(symbol, arguments, block) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
check_expiry
check_responder_responds_to(symbol)
invocation = Invocation.new(self, symbol, arguments, block)

matching_expectations = all_expectations.matching_expectations(invocation)
matching_expectation_allowing_invocation = matching_expectations.detect(&:invocations_allowed?)
matching_expectation_never_allowing_invocation = matching_expectations.detect(&:invocations_never_allowed?)

if matching_expectation_allowing_invocation && !matching_expectation_never_allowing_invocation
matching_expectation_allowing_invocation.invoke(invocation)
else
matching_expectation_ignoring_order = all_expectations.match(invocation, ignoring_order: true)
if matching_expectation_ignoring_order || (!matching_expectation_ignoring_order && !@everything_stubbed)
raise_unexpected_invocation_error(invocation, matching_expectation_ignoring_order)
index = 0
while index < matching_expectations.length
matching_expectation = matching_expectations[index]
if matching_expectation.invocations_never_allowed?
raise_unexpected_invocation_error(invocation, matching_expectation)
elsif matching_expectation.invocations_allowed?
return matching_expectation.invoke(invocation)
end
index += 1
end

matching_expectation_ignoring_order = all_expectations.match(invocation, ignoring_order: true)
if matching_expectation_ignoring_order || (!matching_expectation_ignoring_order && !@everything_stubbed) # rubocop:disable Style/GuardClause
raise_unexpected_invocation_error(invocation, matching_expectation_ignoring_order)
end
end

Expand Down
10 changes: 10 additions & 0 deletions test/acceptance/mocked_methods_dispatch_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,14 @@ def test_should_fail_fast_if_invocation_matches_expectation_with_never_cardinali
'- allowed any number of times, invoked never: #<Mock:mock>.method(any_parameters)'
], test_result.failure_message_lines
end

def test_should_not_fail_fast_if_invocation_matches_expectation_allowing_invocation_before_matching_expectation_with_never_cardinality
test_result = run_as_test do
mock = mock('mock')
mock.expects(:method).never
mock.expects(:method).once
mock.method
end
assert_passed(test_result)
end
end

0 comments on commit 29a8e08

Please sign in to comment.