Skip to content

Commit

Permalink
Merge pull request #660 from freerange/fix-regression-in-matching-has…
Browse files Browse the repository at this point in the history
…h-parameter

Fix regression when matching Hash parameter
  • Loading branch information
floehopper authored Jul 22, 2024
2 parents e62fa61 + 5e6a07b commit 6c1be00
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 18 deletions.
7 changes: 5 additions & 2 deletions lib/mocha/parameter_matchers/has_entries.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,23 @@ def has_entries(entries) # rubocop:disable Naming/PredicateName
# Parameter matcher which matches when actual parameter contains all expected +Hash+ entries.
class HasEntries < Base
# @private
def initialize(entries)
def initialize(entries, exact: false)
@entries = entries
@exact = exact
end

# @private
def matches?(available_parameters)
parameter = available_parameters.shift
return false if @exact && @entries.length != parameter.length

has_entry_matchers = @entries.map { |key, value| HasEntry.new(key, value) }
AllOf.new(*has_entry_matchers).matches?([parameter])
end

# @private
def mocha_inspect
"has_entries(#{@entries.mocha_inspect})"
@exact ? @entries.mocha_inspect : "has_entries(#{@entries.mocha_inspect})"
end
end
end
Expand Down
3 changes: 2 additions & 1 deletion lib/mocha/parameter_matchers/positional_or_keyword_hash.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'mocha/configuration'
require 'mocha/deprecation'
require 'mocha/parameter_matchers/base'
require 'mocha/parameter_matchers/has_entries'

module Mocha
module ParameterMatchers
Expand All @@ -14,7 +15,7 @@ def initialize(value, expectation)
def matches?(available_parameters)
parameter, is_last_parameter = extract_parameter(available_parameters)

return false unless HasEntries.new(@value).matches?([parameter])
return false unless HasEntries.new(@value, exact: true).matches?([parameter])

if is_last_parameter && !same_type_of_hash?(parameter, @value)
return false if Mocha.configuration.strict_keyword_argument_matching?
Expand Down
45 changes: 45 additions & 0 deletions test/acceptance/parameter_matcher_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@ def teardown
teardown_acceptance_test
end

def test_should_match_hash_parameter_which_is_exactly_the_same
test_result = run_as_test do
mock = mock()
mock.expects(:method).with(key_1: 'value_1')
mock.method(key_1: 'value_1')
end
assert_passed(test_result)
end

def test_should_not_match_hash_parameter_which_is_not_exactly_the_same
test_result = run_as_test do
mock = mock()
mock.expects(:method).with(key_1: 'value_1')
mock.method(key_1: 'value_1', key_2: 'value_2')
end
assert_failed(test_result)
end

def test_should_match_hash_parameter_with_specified_key
test_result = run_as_test do
mock = mock()
Expand Down Expand Up @@ -137,6 +155,33 @@ def test_should_not_match_hash_parameter_with_specified_entries_using_nested_mat
assert_failed(test_result)
end

def test_should_match_hash_parameter_that_is_exactly_a_key_that_is_a_string_with_a_value_that_is_an_integer
test_result = run_as_test do
mock = mock()
mock.expects(:method).with(is_a(String) => is_a(Integer))
mock.method('key_1' => 123)
end
assert_passed(test_result)
end

def test_should_not_match_hash_parameter_that_is_exactly_a_key_that_is_a_string_with_a_value_that_is_an_integer_because_value_not_integer
test_result = run_as_test do
mock = mock()
mock.expects(:method).with(is_a(String) => is_a(Integer))
mock.method('key_1' => '123')
end
assert_failed(test_result)
end

def test_should_not_match_hash_parameter_that_is_exactly_a_key_that_is_a_string_with_a_value_that_is_an_integer_because_of_extra_entry
test_result = run_as_test do
mock = mock()
mock.expects(:method).with(is_a(String) => is_a(Integer))
mock.method('key_1' => 123, 'key_2' => 'doesntmatter')
end
assert_failed(test_result)
end

def test_should_match_parameter_that_matches_any_value
test_result = run_as_test do
mock = mock()
Expand Down
60 changes: 45 additions & 15 deletions test/unit/parameter_matchers/positional_or_keyword_hash_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,38 +11,58 @@ class PositionalOrKeywordHashTest < Mocha::TestCase
include Mocha::ParameterMatchers

def test_should_describe_matcher
matcher = { key_1: 1, key_2: 2 }.to_matcher
hash = { key_1: 1, key_2: 2 }
matcher = build_matcher(hash)
assert_equal '{:key_1 => 1, :key_2 => 2}', matcher.mocha_inspect
end

def test_should_match_non_last_hash_arg_with_hash_arg
matcher = { key_1: 1, key_2: 2 }.to_matcher
hash = { key_1: 1, key_2: 2 }
matcher = build_matcher(hash)
assert matcher.matches?([{ key_1: 1, key_2: 2 }, %w[a b]])
end

def test_should_not_match_non_hash_arg_with_hash_arg
matcher = { key_1: 1, key_2: 2 }.to_matcher
hash = { key_1: 1, key_2: 2 }
matcher = build_matcher(hash)
assert !matcher.matches?([%w[a b]])
end

def test_should_match_hash_arg_with_hash_arg
matcher = { key_1: 1, key_2: 2 }.to_matcher
hash = { key_1: 1, key_2: 2 }
matcher = build_matcher(hash)
assert matcher.matches?([{ key_1: 1, key_2: 2 }])
end

def test_should_not_match_hash_arg_with_different_hash_arg
hash = { key_1: 1 }
matcher = build_matcher(hash)
assert !matcher.matches?([{ key_1: 1, key_2: 2 }])
end

def test_should_match_keyword_args_with_keyword_args
matcher = Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 }).to_matcher # rubocop:disable Style/BracesAroundHashParameters
matcher = build_matcher(Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })) # rubocop:disable Style/BracesAroundHashParameters
assert matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })]) # rubocop:disable Style/BracesAroundHashParameters
end

def test_should_not_match_keyword_args_with_different_keyword_args
matcher = build_matcher(Hash.ruby2_keywords_hash({ key_1: 1 })) # rubocop:disable Style/BracesAroundHashParameters
assert !matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })]) # rubocop:disable Style/BracesAroundHashParameters
end

def test_should_match_keyword_args_with_matchers_using_keyword_args
matcher = Hash.ruby2_keywords_hash({ key_1: is_a(String), key_2: is_a(Integer) }).to_matcher(top_level: true) # rubocop:disable Style/BracesAroundHashParameters
matcher = build_matcher(Hash.ruby2_keywords_hash({ key_1: is_a(String), key_2: is_a(Integer) })) # rubocop:disable Style/BracesAroundHashParameters
assert matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 'foo', key_2: 2 })]) # rubocop:disable Style/BracesAroundHashParameters
end

def test_should_not_match_keyword_args_with_matchers_using_keyword_args_when_not_all_entries_are_matched
matcher = build_matcher(Hash.ruby2_keywords_hash({ key_1: is_a(String) })) # rubocop:disable Style/BracesAroundHashParameters
assert !matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 'foo', key_2: 2 })]) # rubocop:disable Style/BracesAroundHashParameters
end

def test_should_match_hash_arg_with_keyword_args_but_display_deprecation_warning_if_appropriate
expectation = Mocha::Expectation.new(self, :foo); execution_point = ExecutionPoint.current
matcher = Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 }).to_matcher(expectation: expectation, top_level: true) # rubocop:disable Style/BracesAroundHashParameters
matcher = build_matcher(Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 }), expectation) # rubocop:disable Style/BracesAroundHashParameters
DeprecationDisabler.disable_deprecations do
assert matcher.matches?([{ key_1: 1, key_2: 2 }])
end
Expand All @@ -58,7 +78,7 @@ def test_should_match_hash_arg_with_keyword_args_but_display_deprecation_warning

def test_should_match_keyword_args_with_hash_arg_but_display_deprecation_warning_if_appropriate
expectation = Mocha::Expectation.new(self, :foo); execution_point = ExecutionPoint.current
matcher = { key_1: 1, key_2: 2 }.to_matcher(expectation: expectation, top_level: true)
matcher = build_matcher({ key_1: 1, key_2: 2 }, expectation)
DeprecationDisabler.disable_deprecations do
assert matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })]) # rubocop:disable Style/BracesAroundHashParameters
end
Expand All @@ -74,50 +94,54 @@ def test_should_match_keyword_args_with_hash_arg_but_display_deprecation_warning

if Mocha::RUBY_V27_PLUS
def test_should_match_non_last_hash_arg_with_hash_arg_when_strict_keyword_args_is_enabled
matcher = { key_1: 1, key_2: 2 }.to_matcher
hash = { key_1: 1, key_2: 2 }
matcher = build_matcher(hash)
Mocha::Configuration.override(strict_keyword_argument_matching: true) do
assert matcher.matches?([{ key_1: 1, key_2: 2 }, %w[a b]])
end
end

def test_should_not_match_non_hash_arg_with_hash_arg_when_strict_keyword_args_is_enabled
matcher = { key_1: 1, key_2: 2 }.to_matcher
hash = { key_1: 1, key_2: 2 }
matcher = build_matcher(hash)
Mocha::Configuration.override(strict_keyword_argument_matching: true) do
assert !matcher.matches?([%w[a b]])
end
end

def test_should_match_hash_arg_with_hash_arg_when_strict_keyword_args_is_enabled
matcher = { key_1: 1, key_2: 2 }.to_matcher
hash = { key_1: 1, key_2: 2 }
matcher = build_matcher(hash)
Mocha::Configuration.override(strict_keyword_argument_matching: true) do
assert matcher.matches?([{ key_1: 1, key_2: 2 }])
end
end

def test_should_match_keyword_args_with_keyword_args_when_strict_keyword_args_is_enabled
matcher = Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 }).to_matcher # rubocop:disable Style/BracesAroundHashParameters
matcher = build_matcher(Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })) # rubocop:disable Style/BracesAroundHashParameters
Mocha::Configuration.override(strict_keyword_argument_matching: true) do
assert matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })]) # rubocop:disable Style/BracesAroundHashParameters
end
end

def test_should_not_match_hash_arg_with_keyword_args_when_strict_keyword_args_is_enabled
matcher = Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 }).to_matcher(top_level: true) # rubocop:disable Style/BracesAroundHashParameters
matcher = build_matcher(Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })) # rubocop:disable Style/BracesAroundHashParameters
Mocha::Configuration.override(strict_keyword_argument_matching: true) do
assert !matcher.matches?([{ key_1: 1, key_2: 2 }])
end
end

def test_should_not_match_keyword_args_with_hash_arg_when_strict_keyword_args_is_enabled
matcher = { key_1: 1, key_2: 2 }.to_matcher(top_level: true)
hash = { key_1: 1, key_2: 2 }
matcher = build_matcher(hash)
Mocha::Configuration.override(strict_keyword_argument_matching: true) do
assert !matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })]) # rubocop:disable Style/BracesAroundHashParameters
end
end

def test_should_display_deprecation_warning_even_if_parent_expectation_is_nil
expectation = nil
matcher = { key_1: 1, key_2: 2 }.to_matcher(expectation: expectation, top_level: true)
matcher = build_matcher({ key_1: 1, key_2: 2 }, expectation)
DeprecationDisabler.disable_deprecations do
matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })]) # rubocop:disable Style/BracesAroundHashParameters
end
Expand All @@ -127,4 +151,10 @@ def test_should_display_deprecation_warning_even_if_parent_expectation_is_nil
assert_includes message, 'but received keyword arguments (key_1: 1, key_2: 2)'
end
end

private

def build_matcher(hash, expectation = nil)
Mocha::ParameterMatchers::PositionalOrKeywordHash.new(hash, expectation)
end
end

0 comments on commit 6c1be00

Please sign in to comment.