Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix strict kwargs matching when method doesn't accept kwargs #605

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/mocha/class_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def initialize(klass)

def mocha(instantiate = true)
if instantiate
@mocha ||= Mocha::Mockery.instance.mock_impersonating_any_instance_of(@stubba_object)
@mocha ||= Mocha::Mockery.instance.mock_impersonating_any_instance_of(@stubba_object).responds_like_instance_of(@stubba_object)
else
defined?(@mocha) ? @mocha : nil
end
Expand Down
2 changes: 1 addition & 1 deletion lib/mocha/expectation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ def matches_method?(method_name)

# @private
def match?(invocation)
@method_matcher.match?(invocation.method_name) && @parameters_matcher.match?(invocation.arguments) && @block_matcher.match?(invocation.block) && in_correct_order?
@method_matcher.match?(invocation.method_name) && @parameters_matcher.match?(invocation) && @block_matcher.match?(invocation.block) && in_correct_order?
end

# @private
Expand Down
9 changes: 8 additions & 1 deletion lib/mocha/invocation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ module Mocha
class Invocation
attr_reader :method_name, :block

def initialize(mock, method_name, arguments = [], block = nil)
def initialize(mock, method_name, arguments = [], block = nil, responder = nil)
@mock = mock
@method_name = method_name
@arguments = arguments
@block = block
@responder = responder
@yields = []
@result = nil
end
Expand Down Expand Up @@ -62,6 +63,12 @@ def full_description
"\n - #{call_description} #{result_description}"
end

def method_accepts_keyword_arguments?
return true unless @responder

@responder.method(@method_name).parameters.any? { |k, v| %i(keyreq key keyrest).include?(k) }
end

private

def argument_description
Expand Down
13 changes: 9 additions & 4 deletions lib/mocha/mock.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require 'mocha/parameters_matcher'
require 'mocha/argument_iterator'
require 'mocha/expectation_error_factory'
require 'mocha/ruby_version'

module Mocha
# Traditional mock object.
Expand Down Expand Up @@ -320,7 +321,7 @@ def method_missing(symbol, *arguments, &block)
def handle_method_call(symbol, arguments, block)
check_expiry
check_responder_responds_to(symbol)
invocation = Invocation.new(self, symbol, arguments, block)
invocation = Invocation.new(self, symbol, arguments, block, @responder)
if (matching_expectation_allowing_invocation = all_expectations.match_allowing_invocation(invocation))
matching_expectation_allowing_invocation.invoke(invocation)
elsif (matching_expectation = all_expectations.match(invocation)) || (!matching_expectation && !@everything_stubbed)
Expand Down Expand Up @@ -381,9 +382,13 @@ def raise_unexpected_invocation_error(invocation, matching_expectation)
end

def check_responder_responds_to(symbol)
if @responder && !@responder.respond_to?(symbol) # rubocop:disable Style/GuardClause
raise NoMethodError, "undefined method `#{symbol}' for #{mocha_inspect} which responds like #{@responder.mocha_inspect}"
end
return unless @responder

legacy_behaviour_for_array_flatten = !RUBY_V23_PLUS && !@responder.respond_to?(symbol) && (symbol == :to_ary)

return if @responder.respond_to?(symbol, true) && !legacy_behaviour_for_array_flatten

raise NoMethodError, "undefined method `#{symbol}' for #{mocha_inspect} which responds like #{@responder.mocha_inspect}"
end

def check_expiry
Expand Down
2 changes: 1 addition & 1 deletion lib/mocha/object_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ module ObjectMethods
# @private
def mocha(instantiate = true)
if instantiate
@mocha ||= Mocha::Mockery.instance.mock_impersonating(self)
@mocha ||= Mocha::Mockery.instance.mock_impersonating(self).responds_like(self)
else
defined?(@mocha) ? @mocha : nil
end
Expand Down
10 changes: 7 additions & 3 deletions lib/mocha/parameter_matchers/instance_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module ParameterMatchers
# @private
module InstanceMethods
# @private
def to_matcher(_expectation = nil)
def to_matcher(_expectation = nil, _method_accepts_keyword_arguments = true)
Mocha::ParameterMatchers::Equals.new(self)
end
end
Expand All @@ -21,7 +21,11 @@ class Object
# @private
class Hash
# @private
def to_matcher(expectation = nil)
Mocha::ParameterMatchers::PositionalOrKeywordHash.new(self, expectation)
def to_matcher(expectation = nil, method_accepts_keyword_arguments = true)
if method_accepts_keyword_arguments
Mocha::ParameterMatchers::PositionalOrKeywordHash.new(self, expectation)
else
Mocha::ParameterMatchers::Equals.new(self)
end
end
end
13 changes: 5 additions & 8 deletions lib/mocha/parameters_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,23 @@ def initialize(expected_parameters = [ParameterMatchers::AnyParameters.new], exp
@matching_block = matching_block
end

def match?(actual_parameters = [])
def match?(invocation)
actual_parameters = invocation.arguments || []
if @matching_block
@matching_block.call(*actual_parameters)
else
parameters_match?(actual_parameters)
matchers(invocation).all? { |matcher| matcher.matches?(actual_parameters) } && actual_parameters.empty?
end
end

def parameters_match?(actual_parameters)
matchers.all? { |matcher| matcher.matches?(actual_parameters) } && actual_parameters.empty?
end

def mocha_inspect
signature = matchers.mocha_inspect
signature = signature.gsub(/^\[|\]$/, '')
"(#{signature})"
end

def matchers
@expected_parameters.map { |p| p.to_matcher(@expectation) }
def matchers(invocation)
@expected_parameters.map { |p| p.to_matcher(@expectation, invocation.method_accepts_keyword_arguments?) }
end
end
end
1 change: 1 addition & 0 deletions lib/mocha/ruby_version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
module Mocha
RUBY_V23_PLUS = Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.3')
RUBY_V27_PLUS = Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.7')
end
16 changes: 8 additions & 8 deletions test/acceptance/responds_like_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,14 @@ def foo; end
assert_passed(test_result)
end

def test_mock_which_responds_like_object_with_protected_method_raises_no_method_error_when_method_is_not_stubbed
def test_mock_which_responds_like_object_with_protected_method_raises_unexpected_invocation_exception_when_method_is_not_stubbed
object = Class.new do
def foo; end
protected :foo
end.new
test_result = run_as_test do
m = mock.responds_like(object)
assert_raises(NoMethodError) { m.foo } # vs Minitest::Assertion for public method
assert_raises(Minitest::Assertion) { m.foo }
end
assert_passed(test_result)
end
Expand Down Expand Up @@ -168,15 +168,15 @@ def foo; end
assert_passed(test_result)
end

def test_mock_which_responds_like_object_with_protected_method_raises_no_method_error_when_method_is_stubbed
def test_mock_which_responds_like_object_with_protected_method_does_not_raise_exception_when_method_is_stubbed
object = Class.new do
def foo; end
protected :foo
end.new
test_result = run_as_test do
m = mock.responds_like(object)
m.stubs(:foo)
assert_raises(NoMethodError) { m.foo } # vs no exception for public method
assert_nil m.foo
end
assert_passed(test_result)
end
Expand All @@ -196,14 +196,14 @@ def foo; end
assert_passed(test_result)
end

def test_mock_which_responds_like_object_with_private_method_raises_no_method_error_when_method_is_not_stubbed
def test_mock_which_responds_like_object_with_private_method_raises_unexpected_invocation_exception_when_method_is_not_stubbed
object = Class.new do
def foo; end
private :foo
end.new
test_result = run_as_test do
m = mock.responds_like(object)
assert_raises(NoMethodError) { m.foo } # vs Minitest::Assertion for public method
assert_raises(Minitest::Assertion) { m.foo }
end
assert_passed(test_result)
end
Expand Down Expand Up @@ -234,15 +234,15 @@ def foo; end
assert_passed(test_result)
end

def test_mock_which_responds_like_object_with_private_method_raises_no_method_error_when_method_is_stubbed
def test_mock_which_responds_like_object_with_private_method_does_not_raise_exception_when_method_is_stubbed
object = Class.new do
def foo; end
private :foo
end.new
test_result = run_as_test do
m = mock.responds_like(object)
m.stubs(:foo)
assert_raises(NoMethodError) { m.foo } # vs no exception for public method
assert_nil m.foo
end
assert_passed(test_result)
end
Expand Down