-
-
Notifications
You must be signed in to change notification settings - Fork 158
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support strict keyword argument matching
Closes #562. This introduces a new `strict_keyword_argument_matching` configuration option. This option is only available in Ruby >= v2.7 and is disabled by default to enable gradual adoption. When the strict keyword argument option is enabled, an expectation expecting keyword arguments (via `Expectation#with`) will no longer match an invocation passing a positional Hash argument. Without this option, positional hash and keyword arguments are treated the same during comparison, which can lead to false negatives in Ruby >= v3.0 (see examples below). * Loose keyword argument matching (default) class Example def foo(a, bar:); end end example = Example.new example.expects(:foo).with('a', bar: 'b') example.foo('a', { bar: 'b' }) # This passes the test, but would result in an ArgumentError in practice * Strict keyword argument matching Mocha.configure do |c| c.strict_keyword_argument_matching = true end class Example def foo(a, bar:); end end example = Example.new example.expects(:foo).with('a', bar: 'b') example.foo('a', { bar: 'b' }) # This now fails as expected For more details on keyword arguments in Ruby v3, refer to this article [1]. 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. Closes #446. See also #535 & #544 for discussions relating to this change. [1]: https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0 Co-authored-by: Nicholas Koh <[email protected]>
- Loading branch information
Showing
18 changed files
with
522 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,3 +28,5 @@ if ENV['MOCHA_GENERATE_DOCS'] | |
gem 'redcarpet' | ||
gem 'yard' | ||
end | ||
|
||
gem 'ruby2_keywords', '~> 0.0.5' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
lib/mocha/parameter_matchers/positional_or_keyword_hash.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
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) | ||
return false unless parameter.is_a?(Hash) | ||
|
||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
require 'mocha/deprecation' | ||
|
||
module Mocha | ||
RUBY_V27_PLUS = Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.7') | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
require File.expand_path('../acceptance_test_helper', __FILE__) | ||
|
||
class KeywordArgumentMatchingTest < Mocha::TestCase | ||
include AcceptanceTest | ||
|
||
def setup | ||
setup_acceptance_test | ||
end | ||
|
||
def teardown | ||
teardown_acceptance_test | ||
end | ||
|
||
def test_should_match_hash_parameter_with_keyword_args | ||
test_result = run_as_test do | ||
mock = mock() | ||
mock.expects(:method).with(:key => 42) | ||
mock.method({ :key => 42 }) # rubocop:disable Style/BracesAroundHashParameters | ||
end | ||
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() | ||
mock.expects(:method).with(**{ :key => 42 }) | ||
mock.method({ :key => 42 }) # rubocop:disable Style/BracesAroundHashParameters | ||
end | ||
assert_passed(test_result) | ||
end | ||
|
||
if Mocha::RUBY_V27_PLUS | ||
def test_should_not_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() | ||
mock.expects(:method).with(:key => 42) | ||
mock.method(**{ :key => 42 }) | ||
end | ||
assert_passed(test_result) | ||
end | ||
|
||
def test_should_match_splatted_hash_parameter_with_splatted_hash | ||
test_result = run_as_test do | ||
mock = mock() | ||
mock.expects(:method).with(**{ :key => 42 }) | ||
mock.method(**{ :key => 42 }) | ||
end | ||
assert_passed(test_result) | ||
end | ||
|
||
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 }) # rubocop:disable Style/BracesAroundHashParameters | ||
mock.method(1, :key => 42) | ||
end | ||
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() | ||
mock.expects(:method).with(1, :key => 42) | ||
mock.method(1, { :key => 42 }) # rubocop:disable Style/BracesAroundHashParameters | ||
end | ||
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() | ||
mock.expects(:method).with(1, :key => 42) | ||
mock.method(1, :key => 42) | ||
end | ||
assert_passed(test_result) | ||
end | ||
|
||
def test_should_match_hash_parameter_with_hash_matcher | ||
test_result = run_as_test do | ||
mock = mock() | ||
mock.expects(:method).with(has_key(:key)) | ||
mock.method({ :key => 42 }) # rubocop:disable Style/BracesAroundHashParameters | ||
end | ||
assert_passed(test_result) | ||
end | ||
|
||
def test_should_match_splatted_hash_parameter_with_hash_matcher | ||
test_result = run_as_test do | ||
mock = mock() | ||
mock.expects(:method).with(has_key(:key)) | ||
mock.method(**{ :key => 42 }) | ||
end | ||
assert_passed(test_result) | ||
end | ||
|
||
def test_should_match_positional_and_keyword_args_with_hash_matcher | ||
test_result = run_as_test do | ||
mock = mock() | ||
mock.expects(:method).with(1, has_key(:key)) | ||
mock.method(1, :key => 42) | ||
end | ||
assert_passed(test_result) | ||
end | ||
|
||
def test_should_match_last_positional_hash_with_hash_matcher | ||
test_result = run_as_test do | ||
mock = mock() | ||
mock.expects(:method).with(1, has_key(:key)) | ||
mock.method(1, { :key => 42 }) # rubocop:disable Style/BracesAroundHashParameters | ||
end | ||
assert_passed(test_result) | ||
end | ||
|
||
if Mocha::RUBY_V27_PLUS | ||
def test_should_not_match_non_hash_args_with_keyword_args | ||
test_result = run_as_test do | ||
mock = mock() | ||
mock.expects(:method).with(**{ :key => 1 }) | ||
Mocha::Configuration.override(:strict_keyword_argument_matching => true) do | ||
mock.method([2]) | ||
end | ||
end | ||
assert_failed(test_result) | ||
end | ||
end | ||
end |
Oops, something went wrong.