From 04845ccdc8aa097e7fc002ef4da73d325208860e Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Sat, 5 Nov 2022 16:26:13 +0300 Subject: [PATCH 1/4] Add Ruby 3.2 to CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb31d4c1b..0e43ab2b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,7 @@ jobs: strategy: matrix: ruby: + - '3.2' - '3.1' - '3.0' - 2.7 From 96019c262f8889ff33aadda537b8df97ee3e692c Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Sat, 5 Nov 2022 16:26:01 +0300 Subject: [PATCH 2/4] Add more specs for kwargs forwarding and matching --- lib/rspec/mocks/argument_list_matcher.rb | 4 +- spec/rspec/mocks/argument_matchers_spec.rb | 2 +- spec/rspec/mocks/matchers/receive_spec.rb | 53 ++++++++++++++++++++ spec/rspec/mocks/stub_implementation_spec.rb | 14 ++++++ 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/lib/rspec/mocks/argument_list_matcher.rb b/lib/rspec/mocks/argument_list_matcher.rb index af98478e7..f8adcd931 100644 --- a/lib/rspec/mocks/argument_list_matcher.rb +++ b/lib/rspec/mocks/argument_list_matcher.rb @@ -61,7 +61,9 @@ 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) return false diff --git a/spec/rspec/mocks/argument_matchers_spec.rb b/spec/rspec/mocks/argument_matchers_spec.rb index cd17bb34a..617fb07e5 100644 --- a/spec/rspec/mocks/argument_matchers_spec.rb +++ b/spec/rspec/mocks/argument_matchers_spec.rb @@ -388,7 +388,7 @@ def ==(other) end 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 + it "fails to match 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 diff --git a/spec/rspec/mocks/matchers/receive_spec.rb b/spec/rspec/mocks/matchers/receive_spec.rb index 768eee9de..1b71dd58f 100644 --- a/spec/rspec/mocks/matchers/receive_spec.rb +++ b/spec/rspec/mocks/matchers/receive_spec.rb @@ -31,6 +31,20 @@ module Mocks end end + # FIXME: this is defined here to prevent + # "warning: method redefined; discarding old kw_args_method" + # because shared examples are evaluated several times. + # When we flatten those shared examples in RSpec 4 because + # of no "should" syntax, it will become possible to put this + # class definition closer to examples that use it. + if RSpec::Support::RubyFeatures.required_kw_args_supported? + binding.eval(<<-RUBY, __FILE__, __LINE__) + class TestObject + def kw_args_method(a:, b:); end + end + RUBY + end + shared_examples "a receive matcher" do |*options| it 'allows the caller to configure how the subject responds' do wrapped.to receive(:foo).and_return(5) @@ -109,6 +123,45 @@ module Mocks expect(receiver.foo(kw: :arg)).to eq(:arg) end + + it "expects to receive keyword args" 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 + + if RUBY_VERSION >= '3.0' + it "fails to expect to receive hash with keyword args" 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('expected: ({:a=>1, :b=>2}) (keyword arguments)') + .and include('got: ({:a=>1, :b=>2}) (options hash)') + end + end + else + it "expects to receive hash with keyword args" 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 + 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/stub_implementation_spec.rb b/spec/rspec/mocks/stub_implementation_spec.rb index 2c3cb9327..d87ed1b67 100644 --- a/spec/rspec/mocks/stub_implementation_spec.rb +++ b/spec/rspec/mocks/stub_implementation_spec.rb @@ -92,6 +92,20 @@ def obj.foo; :original; end include_context "with isolated configuration" before { RSpec::Mocks.configuration.verify_partial_doubles = true } include_examples "stubbing `new` on class objects" + + if RSpec::Support::RubyFeatures.required_kw_args_supported? + binding.eval(<<-RUBY, __FILE__, __LINE__) + it "handles keyword arguments correctly" do + klass = Class.new do + def initialize(kw:) + end + end + + allow(klass).to receive(:new).and_call_original + klass.new(kw: 42) + end + RUBY + end end end end From 04567268e283ea35d80bbabaee119133c8b08a22 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Tue, 3 Jan 2023 08:15:29 +0100 Subject: [PATCH 3/4] Fix args forwarding on Ruby 3.2 Related: - https://github.com/rspec/rspec-mocks/pull/1497 - https://github.com/rspec/rspec-mocks/pull/1502 Co-authored-by: Slava Kardakov --- lib/rspec/mocks/verifying_proxy.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/rspec/mocks/verifying_proxy.rb b/lib/rspec/mocks/verifying_proxy.rb index b39871c87..1d8d207dc 100644 --- a/lib/rspec/mocks/verifying_proxy.rb +++ b/lib/rspec/mocks/verifying_proxy.rb @@ -160,6 +160,7 @@ def proxy_method_invoked(obj, *args, &block) validate_arguments!(args) super end + ruby2_keywords :proxy_method_invoked if respond_to?(:ruby2_keywords, true) def validate_arguments!(actual_args) @method_reference.with_signature do |signature| From 672b148cc85570dba660c763d9175bf6c0560bc4 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Tue, 3 Jan 2023 08:30:47 +0100 Subject: [PATCH 4/4] Add changelog entry --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index e7ad4f1fb..5be3700dd 100644 --- a/Changelog.md +++ b/Changelog.md @@ -13,6 +13,8 @@ Bug Fixes: * Prevent a misleading error message when using `allow(...).not_to` with unsupported matchers. (Phil Pirozhkov, #1503) * Fix mocking keyword args for `have_received` with a block. (Adam Steel, #1508) +* Fix mocking keyword args for `received` with Ruby 3.2.0. (Slava Kardakov, + Benoit Tigeot, Phil Pirozhkov, Benoit Daloze, #1514) ### 3.12.0 / 2022-10-26 [Full Changelog](http://github.com/rspec/rspec-mocks/compare/v3.11.2...v3.12.0)