diff --git a/rspec-mocks/lib/rspec/mocks/argument_list_matcher.rb b/rspec-mocks/lib/rspec/mocks/argument_list_matcher.rb index 1ee647bb6..d96dc1e83 100644 --- a/rspec-mocks/lib/rspec/mocks/argument_list_matcher.rb +++ b/rspec-mocks/lib/rspec/mocks/argument_list_matcher.rb @@ -46,16 +46,30 @@ def initialize(*expected_args) @expected_args = expected_args ensure_expected_args_valid! end + ruby2_keywords :initialize if Module.private_method_defined?(:ruby2_keywords) # @api public - # @param [Array] args + # @param [Array] actual_args # # Matches each element in the `expected_args` against the element in the same # position of the arguments passed to `new`. # # @see #initialize - def args_match?(*args) - Support::FuzzyMatcher.values_match?(resolve_expected_args_based_on(args), args) + def args_match?(*actual_args) + expected_args = resolve_expected_args_based_on(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 Hash === expected_args.last && Hash === actual_args.last + if !Hash.ruby2_keywords_hash?(actual_args.last) && Hash.ruby2_keywords_hash?(expected_args.last) + return false + end + end + end + + Support::FuzzyMatcher.values_match?(expected_args, actual_args) end # @private diff --git a/rspec-mocks/lib/rspec/mocks/matchers/receive.rb b/rspec-mocks/lib/rspec/mocks/matchers/receive.rb index 980a01dde..f1ac58684 100644 --- a/rspec-mocks/lib/rspec/mocks/matchers/receive.rb +++ b/rspec-mocks/lib/rspec/mocks/matchers/receive.rb @@ -62,6 +62,7 @@ def setup_any_instance_allowance(subject, &block) @recorded_customizations << ExpectationCustomization.new(method, args, block) self end + ruby2_keywords(method) if Module.private_method_defined?(:ruby2_keywords) end private diff --git a/rspec-mocks/lib/rspec/mocks/message_expectation.rb b/rspec-mocks/lib/rspec/mocks/message_expectation.rb index bd2d465a0..aa9aab600 100644 --- a/rspec-mocks/lib/rspec/mocks/message_expectation.rb +++ b/rspec-mocks/lib/rspec/mocks/message_expectation.rb @@ -364,6 +364,7 @@ def with(*args, &block) @argument_list_matcher = ArgumentListMatcher.new(*args) self end + ruby2_keywords(:with) if Module.private_method_defined?(:ruby2_keywords) # Expect messages to be received in a specific order. # diff --git a/rspec-mocks/spec/rspec/mocks/argument_matchers_spec.rb b/rspec-mocks/spec/rspec/mocks/argument_matchers_spec.rb index 3df91728d..cd17bb34a 100644 --- a/rspec-mocks/spec/rspec/mocks/argument_matchers_spec.rb +++ b/rspec-mocks/spec/rspec/mocks/argument_matchers_spec.rb @@ -381,16 +381,26 @@ def ==(other) a_double.random_call(:a => "a", :b => "b") end - it "matches against a hash submitted by reference and received by value" do + it "matches against a hash submitted as keyword arguments a and received as a positional argument (in both Ruby 2 and Ruby 3)" 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 by value and received by reference" do - opts = {:a => "a", :b => "b"} - expect(a_double).to receive(:random_call).with(:a => "a", :b => "b") - a_double.random_call(opts) + if RUBY_VERSION >= "3" + it "fails to matches against a hash submitted as a positional argument and received as keyword arguments in Ruby 3.0 or later", :reset => true do + opts = {:a => "a", :b => "b"} + expect(a_double).to receive(:random_call).with(:a => "a", :b => "b") + expect do + a_double.random_call(opts) + end.to fail_with(/expected: \(\{(:a=>\"a\", :b=>\"b\"|:b=>\"b\", :a=>\"a\")\}\)/) + end + else + it "matches against a hash submitted as a positional argument and received as keyword arguments in Ruby 2.7 or before" do + opts = {:a => "a", :b => "b"} + expect(a_double).to receive(:random_call).with(:a => "a", :b => "b") + a_double.random_call(opts) + end end it "fails for a hash w/ wrong values", :reset => true do diff --git a/rspec-mocks/spec/rspec/mocks/partial_double_spec.rb b/rspec-mocks/spec/rspec/mocks/partial_double_spec.rb index 39b929f58..099b15517 100644 --- a/rspec-mocks/spec/rspec/mocks/partial_double_spec.rb +++ b/rspec-mocks/spec/rspec/mocks/partial_double_spec.rb @@ -81,10 +81,15 @@ def call(name) expect(object.foobar(:key => "value")).to equal(1) end - it "can accept an inner hash as a message argument" do - hash = {:a => {:key => "value"}} - expect(object).to receive(:foobar).with(:key => "value").and_return(1) - expect(object.foobar(hash[:a])).to equal(1) + if RSpec::Support::RubyFeatures.required_kw_args_supported? + # Use eval to avoid syntax error on 1.8 and 1.9 + binding.eval(<<-CODE, __FILE__, __LINE__) + it "can accept an inner hash as a message argument" do + hash = {:a => {:key => "value"}} + expect(object).to receive(:foobar).with(:key => "value").and_return(1) + expect(object.foobar(**hash[:a])).to equal(1) + end + CODE end it "can create a positive message expectation" do diff --git a/rspec-mocks/spec/rspec/mocks/should_syntax_spec.rb b/rspec-mocks/spec/rspec/mocks/should_syntax_spec.rb index 255f5be21..bef8a5ab1 100644 --- a/rspec-mocks/spec/rspec/mocks/should_syntax_spec.rb +++ b/rspec-mocks/spec/rspec/mocks/should_syntax_spec.rb @@ -463,39 +463,79 @@ def use_rspec_mocks after(:all) { RSpec::Mocks.configuration.syntax = orig_syntax } before { RSpec::Mocks.configuration.reset_syntaxes_to_default } - let(:expected_arguments) { - [ - /Using.*without explicitly enabling/, + if RSpec::Support::RubyFeatures.required_kw_args_supported? + let(:expected_arguments) { + [ + /Using.*without explicitly enabling/, + ] + } + let(:expected_keywords) { {:replacement => "the new `:expect` syntax or explicitly enable `:should`"} - ] - } - - it "it warns about should once, regardless of how many times it is called" do - expect(RSpec).to receive(:deprecate).with(*expected_arguments) - o = Object.new - o2 = Object.new - o.should_receive(:bees) - o2.should_receive(:bees) - - o.bees - o2.bees - end - - it "warns about should not once, regardless of how many times it is called" do - expect(RSpec).to receive(:deprecate).with(*expected_arguments) - o = Object.new - o2 = Object.new - o.should_not_receive(:bees) - o2.should_not_receive(:bees) - end - - it "warns about stubbing once, regardless of how many times it is called" do - expect(RSpec).to receive(:deprecate).with(*expected_arguments) - o = Object.new - o2 = Object.new - - o.stub(:faces) - o2.stub(:faces) + } + it "it warns about should once, regardless of how many times it is called" do + # Use eval to avoid syntax error on 1.8 and 1.9 + eval("expect(RSpec).to receive(:deprecate).with(*expected_arguments, **expected_keywords)") + o = Object.new + o2 = Object.new + o.should_receive(:bees) + o2.should_receive(:bees) + + o.bees + o2.bees + end + + it "warns about should not once, regardless of how many times it is called" do + # Use eval to avoid syntax error on 1.8 and 1.9 + eval("expect(RSpec).to receive(:deprecate).with(*expected_arguments, **expected_keywords)") + o = Object.new + o2 = Object.new + o.should_not_receive(:bees) + o2.should_not_receive(:bees) + end + + it "warns about stubbing once, regardless of how many times it is called" do + # Use eval to avoid syntax error on 1.8 and 1.9 + eval("expect(RSpec).to receive(:deprecate).with(*expected_arguments, **expected_keywords)") + o = Object.new + o2 = Object.new + + o.stub(:faces) + o2.stub(:faces) + end + else + let(:expected_arguments) { + [ + /Using.*without explicitly enabling/, + {:replacement => "the new `:expect` syntax or explicitly enable `:should`"} + ] + } + it "it warns about should once, regardless of how many times it is called" do + expect(RSpec).to receive(:deprecate).with(*expected_arguments) + o = Object.new + o2 = Object.new + o.should_receive(:bees) + o2.should_receive(:bees) + + o.bees + o2.bees + end + + it "warns about should not once, regardless of how many times it is called" do + expect(RSpec).to receive(:deprecate).with(*expected_arguments) + o = Object.new + o2 = Object.new + o.should_not_receive(:bees) + o2.should_not_receive(:bees) + end + + it "warns about stubbing once, regardless of how many times it is called" do + expect(RSpec).to receive(:deprecate).with(*expected_arguments) + o = Object.new + o2 = Object.new + + o.stub(:faces) + o2.stub(:faces) + end end it "warns about unstubbing once, regardless of how many times it is called" do