diff --git a/lib/mocha/configuration.rb b/lib/mocha/configuration.rb index 1e8cfb618..6fdbd7779 100644 --- a/lib/mocha/configuration.rb +++ b/lib/mocha/configuration.rb @@ -306,16 +306,16 @@ def reinstate_undocumented_behaviour_from_v1_9? @options[:reinstate_undocumented_behaviour_from_v1_9] end - # Configure whether to perform strict keyword argument comparision. Only supported in Ruby >= 2.7. + # Configure whether to perform strict keyword argument comparision. Only supported in Ruby >= v2.7. # # Without this option, positional hash and keyword arguments are treated the same during comparison, which can lead to false - # negatives in Ruby >= 3.0 (see examples below). For more details on keyword arguments in Ruby 3, refer to the relevant - # {https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0 blog post}. + # negatives in Ruby >= v3.0 (see examples below). For more details on keyword arguments in Ruby v3, refer to + # {https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0 this article}. # # Note that Hash matchers such as +has_value+ or +has_key+ will still treat positional hash and keyword arguments the same, # so false negatives are still possible when they are used. # - # This is turned off by default to enable gradual adoption, and may be turned on by default in the future. + # This configuration option is turned off by default to enable gradual adoption, but may be turned on by default in the future. # # When +value+ is +true+, strict keyword argument matching will be enabled. # When +value+ is +false+, strict keyword argument matching is disabled. This is the default. diff --git a/lib/mocha/parameter_matchers/instance_methods.rb b/lib/mocha/parameter_matchers/instance_methods.rb index 5bb229cd4..e4430d675 100644 --- a/lib/mocha/parameter_matchers/instance_methods.rb +++ b/lib/mocha/parameter_matchers/instance_methods.rb @@ -1,4 +1,5 @@ require 'mocha/parameter_matchers/equals' +require 'mocha/parameter_matchers/positional_or_keyword_hash' module Mocha module ParameterMatchers @@ -16,3 +17,11 @@ def to_matcher class Object include Mocha::ParameterMatchers::InstanceMethods end + +# @private +class Hash + # @private + def to_matcher + Mocha::ParameterMatchers::PositionalOrKeywordHash.new(self) + end +end diff --git a/lib/mocha/parameter_matchers/positional_or_keyword_hash.rb b/lib/mocha/parameter_matchers/positional_or_keyword_hash.rb new file mode 100644 index 000000000..a34a682f1 --- /dev/null +++ b/lib/mocha/parameter_matchers/positional_or_keyword_hash.rb @@ -0,0 +1,31 @@ +require 'mocha/configuration' +require 'mocha/parameter_matchers/base' + +module Mocha + module ParameterMatchers + # @private + class PositionalOrKeywordHash < Base + def initialize(value) + @value = value + end + + def matches?(available_parameters) + parameter, is_last_parameter = extract_parameter(available_parameters) + if is_last_parameter && Mocha.configuration.strict_keyword_argument_matching? + return false unless ::Hash.ruby2_keywords_hash?(parameter) == ::Hash.ruby2_keywords_hash?(@value) + end + parameter == @value + end + + def mocha_inspect + @value.mocha_inspect + end + + private + + def extract_parameter(available_parameters) + [available_parameters.shift, available_parameters.empty?] + end + end + end +end diff --git a/test/acceptance/keyword_argument_matching_test.rb b/test/acceptance/keyword_argument_matching_test.rb index 50e1b400a..4c0a04507 100644 --- a/test/acceptance/keyword_argument_matching_test.rb +++ b/test/acceptance/keyword_argument_matching_test.rb @@ -20,6 +20,19 @@ def test_should_match_hash_parameter_with_keyword_args assert_passed(test_result) end + if Mocha::RUBY_V27_PLUS + def test_should_not_match_hash_parameter_with_keyword_args_when_strict_keyword_matching_is_enabled + test_result = run_as_test do + mock = mock() + mock.expects(:method).with(:key => 42) + Mocha::Configuration.override(:strict_keyword_argument_matching => true) do + mock.method({ :key => 42 }) # rubocop:disable Style/BracesAroundHashParameters + end + end + assert_failed(test_result) + end + end + def test_should_match_hash_parameter_with_splatted_keyword_args test_result = run_as_test do mock = mock() @@ -29,6 +42,19 @@ def test_should_match_hash_parameter_with_splatted_keyword_args assert_passed(test_result) end + if Mocha::RUBY_V27_PLUS + def test_not_should_match_hash_parameter_with_splatted_keyword_args_when_strict_keyword_matching_is_enabled + test_result = run_as_test do + mock = mock() + mock.expects(:method).with(**{ :key => 42 }) + Mocha::Configuration.override(:strict_keyword_argument_matching => true) do + mock.method({ :key => 42 }) # rubocop:disable Style/BracesAroundHashParameters + end + end + assert_failed(test_result) + end + end + def test_should_match_splatted_hash_parameter_with_keyword_args test_result = run_as_test do mock = mock() @@ -56,6 +82,19 @@ def test_should_match_positional_and_keyword_args_with_last_positional_hash assert_passed(test_result) end + if Mocha::RUBY_V27_PLUS + def test_should_not_match_positional_and_keyword_args_with_last_positional_hash_when_strict_keyword_args_is_enabled + test_result = run_as_test do + mock = mock() + mock.expects(:method).with(1, { :key => 42 }) # rubocop:disable Style/BracesAroundHashParameters + Mocha::Configuration.override(:strict_keyword_argument_matching => true) do + mock.method(1, :key => 42) + end + end + assert_failed(test_result) + end + end + def test_should_match_last_positional_hash_with_keyword_args test_result = run_as_test do mock = mock() @@ -65,6 +104,19 @@ def test_should_match_last_positional_hash_with_keyword_args assert_passed(test_result) end + if Mocha::RUBY_V27_PLUS + def test_should_not_match_last_positional_hash_with_keyword_args_when_strict_keyword_args_is_enabled + test_result = run_as_test do + mock = mock() + mock.expects(:method).with(1, :key => 42) + Mocha::Configuration.override(:strict_keyword_argument_matching => true) do + mock.method(1, { :key => 42 }) # rubocop:disable Style/BracesAroundHashParameters + end + end + assert_failed(test_result) + end + end + def test_should_match_positional_and_keyword_args_with_keyword_args test_result = run_as_test do mock = mock()