Repository: freerange/mocha (Ruby)

Issue:
Default `Configuration#strict_keyword_argument_matching` to `true` in Ruby >= v3.0
From [the docs](https://mocha.jamesmead.org/Mocha/Configuration.html#strict_keyword_argument_matching=-instance_method):

> When this option is set to `false` a positional `Hash` and a set of keyword arguments are treated the same during comparison, which can lead to misleading passing tests in Ruby >= v3.0 (see examples below). However, a deprecation warning will be displayed if a positional `Hash` matches a set of keyword arguments or vice versa. This is because [`#strict_keyword_argument_matching=`](https://mocha.jamesmead.org/Mocha/Configuration.html#strict_keyword_argument_matching=-instance_method) will default to `true` in the future.

When strict matching is disabled, deprecation warnings are already displayed when a test would fail if strict matching was enabled. So there's no need to add a specific deprecation warning before changing the configuration default, because the user could simply manually disable strict matching to prevent their tests from failing.


Discussion:

Patch:
diff --git a/lib/mocha/configuration.rb b/lib/mocha/configuration.rb
index 7108a6ee6..be14ab49d 100644
--- a/lib/mocha/configuration.rb
+++ b/lib/mocha/configuration.rb
@@ -43,7 +43,7 @@ class Configuration
       stubbing_non_existent_method: :allow,
       stubbing_non_public_method: :allow,
       display_matching_invocations_on_failure: false,
-      strict_keyword_argument_matching: false
+      strict_keyword_argument_matching: Mocha::RUBY_V30_PLUS
     }.freeze
 
     attr_reader :options
@@ -229,17 +229,21 @@ def display_matching_invocations_on_failure?
 
     # Perform strict keyword argument comparison. Only supported in Ruby >= v2.7.
     #
-    # When this option is set to +false+ a positional +Hash+ and a set of keyword arguments are treated the same during comparison, which can lead to misleading passing tests in Ruby >= v3.0 (see examples below). However, a deprecation warning will be displayed if a positional +Hash+ matches a set of keyword arguments or vice versa. This is because {#strict_keyword_argument_matching=} will default to +true+ in the future.
+    # When this option is set to +false+ a positional +Hash+ and a set of keyword arguments are treated the same during comparison, which can lead to misleading passing tests in Ruby >= v3.0 (see examples below). However, a deprecation warning will be displayed if a positional +Hash+ matches a set of keyword arguments or vice versa.
     #
     # 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+-related matchers such as {ParameterMatchers#has_value} or {ParameterMatchers#has_key} will still treat a positional +Hash+ and a set of keyword arguments the same, so misleading passing tests are still possible when they are used.
     #
-    # This configuration option is +false+ by default to enable gradual adoption, but will be +true+ by default in the future.
+    # This configuration option is +false+ by default in Ruby v2.7 to enable gradual adoption, but +true+ by default in Ruby >= v3.0.
     #
-    # @param [Boolean] value +true+ to enable strict keyword argument matching; +false+ by default.
+    # @param [Boolean] value +true+ to enable strict keyword argument matching.
     #
-    # @example Loose keyword argument matching (default)
+    # @example Loose keyword argument matching (default in Ruby v2.7)
+    #
+    #   Mocha.configure do |c|
+    #     c.strict_keyword_argument_matching = false
+    #   end
     #
     #   class Example
     #     def foo(a, bar:); end
@@ -250,7 +254,7 @@ def display_matching_invocations_on_failure?
     #   example.foo('a', { bar: 'b' })
     #   # This passes the test, but would result in an ArgumentError in practice
     #
-    # @example Strict keyword argument matching
+    # @example Strict keyword argument matching (default in Ruby >= v3.0)
     #
     #   Mocha.configure do |c|
     #     c.strict_keyword_argument_matching = true
diff --git a/lib/mocha/expectation.rb b/lib/mocha/expectation.rb
index edf7d0b9b..339109fb7 100644
--- a/lib/mocha/expectation.rb
+++ b/lib/mocha/expectation.rb
@@ -229,9 +229,9 @@ def at_most_once
     #
     # Positional arguments were separated from keyword arguments in Ruby v3 (see {https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0 this article}). In relation to this a new configuration option ({Configuration#strict_keyword_argument_matching=}) is available in Ruby >= 2.7.
     #
-    # When {Configuration#strict_keyword_argument_matching=} is set to +false+ (which is currently the default), a positional +Hash+ and a set of keyword arguments passed to {#with} are treated the same for the purposes of parameter matching. However, a deprecation warning will be displayed if a positional +Hash+ matches a set of keyword arguments or vice versa. This is because {Configuration#strict_keyword_argument_matching=} will default to +true+ in the future.
+    # When {Configuration#strict_keyword_argument_matching=} is set to +false+ (which is the default in Ruby v2.7), a positional +Hash+ and a set of keyword arguments passed to {#with} are treated the same for the purposes of parameter matching. However, a deprecation warning will be displayed if a positional +Hash+ matches a set of keyword arguments or vice versa.
     #
-    # When {Configuration#strict_keyword_argument_matching=} is set to +true+, an actual positional +Hash+ will not match an expected set of keyword arguments; and vice versa, an actual set of keyword arguments will not match an expected positional +Hash+, i.e. the parameter matching is stricter.
+    # When {Configuration#strict_keyword_argument_matching=} is set to +true+ (which is the default in Ruby >= v3.0), an actual positional +Hash+ will not match an expected set of keyword arguments; and vice versa, an actual set of keyword arguments will not match an expected positional +Hash+, i.e. the parameter matching is stricter.
     #
     # @see ParameterMatchers
     # @see Configuration#strict_keyword_argument_matching=
@@ -264,7 +264,11 @@ def at_most_once
     #   object.expected_method(['string1'], 'any-old-value')
     #   # => verify fails
     #
-    # @example Loose keyword argument matching (default)
+    # @example Loose keyword argument matching (default in Ruby v2.7).
+    #
+    #   Mocha.configure do |c|
+    #     c.strict_keyword_argument_matching = false
+    #   end
     #
     #   class Example
     #     def foo(a, bar:); end
@@ -275,7 +279,7 @@ def at_most_once
     #   example.foo('a', { bar: 'b' })
     #   # This passes the test, but would result in an ArgumentError in practice
     #
-    # @example Strict keyword argument matching
+    # @example Strict keyword argument matching (default in Ruby >= 3.0).
     #
     #   Mocha.configure do |c|
     #     c.strict_keyword_argument_matching = true
diff --git a/lib/mocha/ruby_version.rb b/lib/mocha/ruby_version.rb
index 8bcf79737..7b3c2a7d9 100644
--- a/lib/mocha/ruby_version.rb
+++ b/lib/mocha/ruby_version.rb
@@ -1,4 +1,5 @@
 module Mocha
   RUBY_V27_PLUS = Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.7')
+  RUBY_V30_PLUS = Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('3.0')
   RUBY_V34_PLUS = Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('3.4')
 end


Test patch:
diff --git a/test/acceptance/keyword_argument_matching_test.rb b/test/acceptance/keyword_argument_matching_test.rb
index 5e1478a9b..85ba5b75e 100644
--- a/test/acceptance/keyword_argument_matching_test.rb
+++ b/test/acceptance/keyword_argument_matching_test.rb
@@ -20,8 +20,10 @@ def test_should_match_hash_parameter_with_keyword_args
     test_result = run_as_test do
       mock = mock()
       mock.expects(:method).with(key: 42); execution_point = ExecutionPoint.current
-      DeprecationDisabler.disable_deprecations do
-        mock.method({ key: 42 })
+      Mocha::Configuration.override(strict_keyword_argument_matching: false) do
+        DeprecationDisabler.disable_deprecations do
+          mock.method({ key: 42 })
+        end
       end
       if Mocha::RUBY_V27_PLUS
         location = execution_point.location
@@ -50,8 +52,10 @@ def test_should_match_hash_parameter_with_splatted_keyword_args
       mock = mock()
       kwargs = { key: 42 }
       mock.expects(:method).with(**kwargs); execution_point = ExecutionPoint.current
-      DeprecationDisabler.disable_deprecations do
-        mock.method({ key: 42 })
+      Mocha::Configuration.override(strict_keyword_argument_matching: false) do
+        DeprecationDisabler.disable_deprecations do
+          mock.method({ key: 42 })
+        end
       end
       if Mocha::RUBY_V27_PLUS
         location = execution_point.location
@@ -100,8 +104,10 @@ def test_should_match_positional_and_keyword_args_with_last_positional_hash
     test_result = run_as_test do
       mock = mock()
       mock.expects(:method).with(1, { key: 42 }); execution_point = ExecutionPoint.current
-      DeprecationDisabler.disable_deprecations do
-        mock.method(1, key: 42)
+      Mocha::Configuration.override(strict_keyword_argument_matching: false) do
+        DeprecationDisabler.disable_deprecations do
+          mock.method(1, key: 42)
+        end
       end
       if Mocha::RUBY_V27_PLUS
         location = execution_point.location
@@ -129,8 +135,10 @@ def test_should_match_last_positional_hash_with_keyword_args
     test_result = run_as_test do
       mock = mock()
       mock.expects(:method).with(1, key: 42); execution_point = ExecutionPoint.current
-      DeprecationDisabler.disable_deprecations do
-        mock.method(1, { key: 42 })
+      Mocha::Configuration.override(strict_keyword_argument_matching: false) do
+        DeprecationDisabler.disable_deprecations do
+          mock.method(1, { key: 42 })
+        end
       end
       if Mocha::RUBY_V27_PLUS
         location = execution_point.location
diff --git a/test/unit/configuration_test.rb b/test/unit/configuration_test.rb
index 723ac67cb..eceb6c8af 100644
--- a/test/unit/configuration_test.rb
+++ b/test/unit/configuration_test.rb
@@ -40,8 +40,14 @@ def test_warn_when_temporarily_changes_config_when_given_block
     assert_equal :allow, Mocha.configuration.stubbing_method_unnecessarily
   end
 
-  def test_strict_keyword_argument_matching_works_is_false_by_default
-    assert !Mocha.configuration.strict_keyword_argument_matching?
+  if Mocha::RUBY_V30_PLUS
+    def test_strict_keyword_argument_matching_works_is_true_by_default_in_ruby_3_0_and_above
+      assert Mocha.configuration.strict_keyword_argument_matching?
+    end
+  elsif Mocha::RUBY_V27_PLUS
+    def test_strict_keyword_argument_matching_works_is_false_by_default_in_ruby_2_7
+      assert !Mocha.configuration.strict_keyword_argument_matching?
+    end
   end
 
   if Mocha::RUBY_V27_PLUS
diff --git a/test/unit/parameter_matchers/positional_or_keyword_hash_test.rb b/test/unit/parameter_matchers/positional_or_keyword_hash_test.rb
index dd2618240..66d7b1dcf 100644
--- a/test/unit/parameter_matchers/positional_or_keyword_hash_test.rb
+++ b/test/unit/parameter_matchers/positional_or_keyword_hash_test.rb
@@ -63,8 +63,10 @@ def test_should_not_match_keyword_args_with_matchers_using_keyword_args_when_not
   def test_should_match_hash_arg_with_keyword_args_but_display_deprecation_warning_if_appropriate
     expectation = Mocha::Expectation.new(self, :foo)
     matcher = build_matcher(Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 }), expectation)
-    DeprecationDisabler.disable_deprecations do
-      assert matcher.matches?([{ key_1: 1, key_2: 2 }])
+    Mocha::Configuration.override(strict_keyword_argument_matching: false) do
+      DeprecationDisabler.disable_deprecations do
+        assert matcher.matches?([{ key_1: 1, key_2: 2 }])
+      end
     end
     return unless Mocha::RUBY_V27_PLUS
 
@@ -79,8 +81,10 @@ 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)
     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 })])
+    Mocha::Configuration.override(strict_keyword_argument_matching: false) do
+      DeprecationDisabler.disable_deprecations do
+        assert matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })])
+      end
     end
     return unless Mocha::RUBY_V27_PLUS
 
@@ -142,8 +146,10 @@ def test_should_not_match_keyword_args_with_hash_arg_when_strict_keyword_args_is
     def test_should_display_deprecation_warning_even_if_parent_expectation_is_nil
       expectation = nil
       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 })])
+      Mocha::Configuration.override(strict_keyword_argument_matching: false) do
+        DeprecationDisabler.disable_deprecations do
+          matcher.matches?([Hash.ruby2_keywords_hash({ key_1: 1, key_2: 2 })])
+        end
       end
 
       message = Mocha::Deprecation.messages.last