diff --git a/lib/rspec/mocks/argument_list_matcher.rb b/lib/rspec/mocks/argument_list_matcher.rb index 9abed7af6..f8adcd931 100644 --- a/lib/rspec/mocks/argument_list_matcher.rb +++ b/lib/rspec/mocks/argument_list_matcher.rb @@ -61,9 +61,11 @@ def args_match?(*actual_args) return false if expected_args.size != actual_args.size if RUBY_VERSION >= "3" - # if both arguments end with Hashes, and if one is a keyword hash and the other is not, they don't match + # If the expectation was set with keywords, while the actual method was called with a positional hash argument, they don't match. + # If the expectation was set without keywords, e.g., with({a: 1}), then it fine to call it with either foo(a: 1) or foo({a: 1}). + # This corresponds to Ruby semantics, as if the method was def foo(options). if Hash === expected_args.last && Hash === actual_args.last - if Hash.ruby2_keywords_hash?(actual_args.last) != Hash.ruby2_keywords_hash?(expected_args.last) + if !Hash.ruby2_keywords_hash?(actual_args.last) && Hash.ruby2_keywords_hash?(expected_args.last) return false end end diff --git a/spec/rspec/mocks/argument_matchers_spec.rb b/spec/rspec/mocks/argument_matchers_spec.rb index 8435a11bc..b5f29b6cf 100644 --- a/spec/rspec/mocks/argument_matchers_spec.rb +++ b/spec/rspec/mocks/argument_matchers_spec.rb @@ -381,23 +381,10 @@ def ==(other) a_double.random_call(:a => "a", :b => "b") end - if RUBY_VERSION >= "3" - it "fails to match against a hash submitted as keyword arguments a and received as a positional argument in Ruby 3" do - expect do - opts = {:a => "a", :b => "b"} - expect(a_double).to receive(:random_call).with(opts) - a_double.random_call(:a => "a", :b => "b") - end.to fail_with do |failure| - expect(failure.message).to include('received unexpected message :random_call with ({:a=>"a", :b=>"b"})') - reset_all - end - end - else - it "matches against a hash submitted as keyword arguments a and received as a positional argument in Ruby 2" do - opts = {:a => "a", :b => "b"} - expect(a_double).to receive(:random_call).with(opts) - a_double.random_call(:a => "a", :b => "b") - end + it "matches against a hash submitted as keyword arguments a and received as a positional argument in Ruby 2" do + opts = {:a => "a", :b => "b"} + expect(a_double).to receive(:random_call).with(opts) + a_double.random_call(:a => "a", :b => "b") end if RUBY_VERSION >= "3" diff --git a/spec/rspec/mocks/matchers/receive_spec.rb b/spec/rspec/mocks/matchers/receive_spec.rb index a2ace4d7f..45dc6e272 100644 --- a/spec/rspec/mocks/matchers/receive_spec.rb +++ b/spec/rspec/mocks/matchers/receive_spec.rb @@ -130,24 +130,30 @@ def kw_args_method(a:, b:); end dbl.kw_args_method(a: 1, b: 2) end - if RUBY_VERSION >= "3" - it "fails to expect to receive keyword args with a hash" do - expect { - dbl = instance_double(TestObject) - expect(dbl).to receive(:kw_args_method).with({a: 1, b: 2}) - dbl.kw_args_method(a: 1, b: 2) - }.to fail_with do |failure| - reset_all - expect(failure.message).to include("received unexpected message :kw_args_method with ({:a=>1, :b=>2})") - end - end - else - it "expects to receive keyword args with a hash" do + it "fails to expect to receive keyword args with a hash" do + expect { dbl = instance_double(TestObject) - expect(dbl).to receive(:kw_args_method).with({a: 1, b: 2}) - dbl.kw_args_method(a: 1, b: 2) + expect(dbl).to receive(:kw_args_method).with(a: 1, b: 2) + dbl.kw_args_method({a: 1, b: 2}) + }.to fail_with do |failure| + reset_all + expect(failure.message) + .to include('expected: ({:a=>1, :b=>2}) (keyword arguments)') + .and include('got: ({:a=>1, :b=>2}) (options hash)') end end + + it "expects to receive hash with a hash" do + dbl = instance_double(TestObject) + expect(dbl).to receive(:kw_args_method).with({a: 1, b: 2}) + dbl.kw_args_method({a: 1, b: 2}) + end + + it "expects to receive keyword args with a hash" do + dbl = instance_double(TestObject) + expect(dbl).to receive(:kw_args_method).with({a: 1, b: 2}) + dbl.kw_args_method(a: 1, b: 2) + end RUBY end end diff --git a/spec/rspec/mocks/verifying_doubles/expected_arg_verification_spec.rb b/spec/rspec/mocks/verifying_doubles/expected_arg_verification_spec.rb index f8a52274c..1167f7453 100644 --- a/spec/rspec/mocks/verifying_doubles/expected_arg_verification_spec.rb +++ b/spec/rspec/mocks/verifying_doubles/expected_arg_verification_spec.rb @@ -125,18 +125,9 @@ module Mocks if RSpec::Support::RubyFeatures.required_kw_args_supported? context "for a method with keyword args" do - if RUBY_VERSION >= "3" - it "fails to match against a hash submitted as keyword arguments and received as positional argument in Ruby 3" do - expect { - expect(dbl).to receive(:kw_args_method).with(1, {:required_arg => 2, :optional_arg => 3}) - dbl.kw_args_method(1, :required_arg => 2, :optional_arg => 3) - }.to fail_with(/received unexpected message :kw_args_method with \(1, \{:optional_arg=>3, :required_arg=>2\}\)/) - end - else - it "matches against a hash submitted as keyword arguments and received as positional argument in Ruby 2" do - expect(dbl).to receive(:kw_args_method).with(1, {:required_arg => 2, :optional_arg => 3}) - dbl.kw_args_method(1, :required_arg => 2, :optional_arg => 3) - end + it "matches against a hash submitted as keyword arguments and received as positional argument (in both Ruby 2 and 3)" do + expect(dbl).to receive(:kw_args_method).with(1, {:required_arg => 2, :optional_arg => 3}) + dbl.kw_args_method(1, :required_arg => 2, :optional_arg => 3) end if RUBY_VERSION >= "3"