From b0ce75beff19b68fa3127ab30829117ff9537a27 Mon Sep 17 00:00:00 2001 From: Jon Rowe Date: Mon, 1 Jun 2020 15:56:42 +0100 Subject: [PATCH] [expectations] Merge pull request rspec/rspec-expectations#1187 from rspec/remove-kw-args-warning-from-has-matcher-without-rspec-support Ensure that has_ matcher works silently with keyword arguments --- This commit was imported from https://github.com/rspec/rspec-expectations/commit/9a2f90c16ca8bf54bb1215c589dc26779bfd3839. --- rspec-expectations/lib/rspec/matchers.rb | 33 +++++++++++----- .../lib/rspec/matchers/built_in/be.rb | 38 +++++++++++++++---- .../lib/rspec/matchers/built_in/has.rb | 28 ++++++++++++-- .../spec/rspec/matchers/built_in/be_spec.rb | 31 +++++++++++++-- .../spec/rspec/matchers/built_in/has_spec.rb | 18 +++++++++ 5 files changed, 124 insertions(+), 24 deletions(-) diff --git a/rspec-expectations/lib/rspec/matchers.rb b/rspec-expectations/lib/rspec/matchers.rb index 20d437c22..f64409a10 100644 --- a/rspec-expectations/lib/rspec/matchers.rb +++ b/rspec-expectations/lib/rspec/matchers.rb @@ -238,7 +238,7 @@ module RSpec # best to find a more positive name for the negated form, such as # `avoid_changing` rather than `not_change`. # - module Matchers + module Matchers # rubocop:disable Metrics/ModuleLength extend ::RSpec::Matchers::DSL # @!macro [attach] alias_matcher @@ -953,14 +953,29 @@ def self.configuration HAS_REGEX = /^(?:have_)(.*)/ DYNAMIC_MATCHER_REGEX = Regexp.union(BE_PREDICATE_REGEX, HAS_REGEX) - def method_missing(method, *args, &block) - case method.to_s - when BE_PREDICATE_REGEX - BuiltIn::BePredicate.new(method, *args, &block) - when HAS_REGEX - BuiltIn::Has.new(method, *args, &block) - else - super + if RSpec::Support::RubyFeatures.kw_args_supported? + binding.eval(<<-CODE, __FILE__, __LINE__) + def method_missing(method, *args, **kwargs, &block) + case method.to_s + when BE_PREDICATE_REGEX + BuiltIn::BePredicate.new(method, *args, **kwargs, &block) + when HAS_REGEX + BuiltIn::Has.new(method, *args, **kwargs, &block) + else + super + end + end + CODE + else + def method_missing(method, *args, &block) + case method.to_s + when BE_PREDICATE_REGEX + BuiltIn::BePredicate.new(method, *args, &block) + when HAS_REGEX + BuiltIn::Has.new(method, *args, &block) + else + super + end end end diff --git a/rspec-expectations/lib/rspec/matchers/built_in/be.rb b/rspec-expectations/lib/rspec/matchers/built_in/be.rb index 0894f97c8..a073ded0a 100644 --- a/rspec-expectations/lib/rspec/matchers/built_in/be.rb +++ b/rspec-expectations/lib/rspec/matchers/built_in/be.rb @@ -181,10 +181,21 @@ def description class BePredicate < BaseMatcher include BeHelpers - def initialize(*args, &block) - @expected = parse_expected(args.shift) - @args = args - @block = block + if RSpec::Support::RubyFeatures.kw_args_supported? + binding.eval(<<-CODE, __FILE__, __LINE__) + def initialize(*args, **kwargs, &block) + @expected = parse_expected(args.shift) + @args = args + @kwargs = kwargs + @block = block + end + CODE + else + def initialize(*args, &block) + @expected = parse_expected(args.shift) + @args = args + @block = block + end end def matches?(actual, &block) @@ -236,9 +247,22 @@ def private_predicate? end end - def predicate_matches? - method_name = actual.respond_to?(predicate) ? predicate : present_tense_predicate - @predicate_matches = actual.__send__(method_name, *@args, &@block) + if RSpec::Support::RubyFeatures.kw_args_supported? + binding.eval(<<-CODE, __FILE__, __LINE__) + def predicate_matches? + method_name = actual.respond_to?(predicate) ? predicate : present_tense_predicate + if @kwargs.empty? + @predicate_matches = actual.__send__(method_name, *@args, &@block) + else + @predicate_matches = actual.__send__(method_name, *@args, **@kwargs, &@block) + end + end + CODE + else + def predicate_matches? + method_name = actual.respond_to?(predicate) ? predicate : present_tense_predicate + @predicate_matches = actual.__send__(method_name, *@args, &@block) + end end def predicate diff --git a/rspec-expectations/lib/rspec/matchers/built_in/has.rb b/rspec-expectations/lib/rspec/matchers/built_in/has.rb index 94a3dc040..cc34fd3b7 100644 --- a/rspec-expectations/lib/rspec/matchers/built_in/has.rb +++ b/rspec-expectations/lib/rspec/matchers/built_in/has.rb @@ -5,8 +5,16 @@ module BuiltIn # Provides the implementation for `has_`. # Not intended to be instantiated directly. class Has < BaseMatcher - def initialize(method_name, *args, &block) - @method_name, @args, @block = method_name, args, block + if RSpec::Support::RubyFeatures.kw_args_supported? + binding.eval(<<-CODE, __FILE__, __LINE__) + def initialize(method_name, *args, **kwargs, &block) + @method_name, @args, @kwargs, @block = method_name, args, kwargs, block + end + CODE + else + def initialize(method_name, *args, &block) + @method_name, @args, @block = method_name, args, block + end end # @private @@ -64,8 +72,20 @@ def predicate_exists? @actual.respond_to? predicate end - def predicate_matches? - @actual.__send__(predicate, *@args, &@block) + if RSpec::Support::RubyFeatures.kw_args_supported? + binding.eval(<<-CODE, __FILE__, __LINE__) + def predicate_matches? + if @kwargs.empty? + @actual.__send__(predicate, *@args, &@block) + else + @actual.__send__(predicate, *@args, **@kwargs, &@block) + end + end + CODE + else + def predicate_matches? + @actual.__send__(predicate, *@args, &@block) + end end def predicate diff --git a/rspec-expectations/spec/rspec/matchers/built_in/be_spec.rb b/rspec-expectations/spec/rspec/matchers/built_in/be_spec.rb index c0c9a18ad..d8f3081f0 100644 --- a/rspec-expectations/spec/rspec/matchers/built_in/be_spec.rb +++ b/rspec-expectations/spec/rspec/matchers/built_in/be_spec.rb @@ -70,15 +70,38 @@ }.to fail_including("expected nil to respond to `happy?`") end + it 'handles arguments to the predicate' do + object = Object.new + def object.predicate?(return_val); return_val; end + expect(object).to be_predicate(true) + expect(object).to_not be_predicate(false) + + expect { expect(object).to be_predicate }.to raise_error(ArgumentError) + expect { expect(object).to be_predicate(false) }.to fail + expect { expect(object).not_to be_predicate(true) }.to fail + end + + it 'handles keyword arguments to the predicate', :if => RSpec::Support::RubyFeatures.required_kw_args_supported? do + object = Object.new + binding.eval(<<-CODE, __FILE__, __LINE__) + def object.predicate?(returns:); returns; end + + expect(object).to be_predicate(returns: true) + expect(object).to_not be_predicate(returns: false) + + expect { expect(object).to be_predicate(returns: false) }.to fail + expect { expect(object).to_not be_predicate(returns: true) }.to fail + CODE + + expect { expect(object).to be_predicate }.to raise_error(ArgumentError) + expect { expect(object).to be_predicate(true) }.to raise_error(ArgumentError) + end + it 'falls back to a present-tense form of the predicate when needed' do mouth = Object.new def mouth.frowns?(return_val); return_val; end expect(mouth).to be_frown(true) - expect(mouth).not_to be_frown(false) - - expect { expect(mouth).to be_frown(false) }.to fail - expect { expect(mouth).not_to be_frown(true) }.to fail end it 'fails when :predicate? is private' do diff --git a/rspec-expectations/spec/rspec/matchers/built_in/has_spec.rb b/rspec-expectations/spec/rspec/matchers/built_in/has_spec.rb index fb740f7dd..55505e8bb 100644 --- a/rspec-expectations/spec/rspec/matchers/built_in/has_spec.rb +++ b/rspec-expectations/spec/rspec/matchers/built_in/has_spec.rb @@ -8,6 +8,24 @@ expect({ :a => "A" }).to have_key(:a) end + if RSpec::Support::RubyFeatures.required_kw_args_supported? + binding.eval(<<-CODE, __FILE__, __LINE__) + it 'supports the use of required keyword arguments' do + thing = Class.new { def has_keyword?(keyword:); keyword == 'a'; end } + expect(thing.new).to have_keyword(keyword: 'a') + end + CODE + end + + if RSpec::Support::RubyFeatures.kw_args_supported? + binding.eval(<<-CODE, __FILE__, __LINE__) + it 'supports the use of optional keyword arguments' do + thing = Class.new { def has_keyword?(keyword: 'b'); keyword == 'a'; end } + expect(thing.new).to have_keyword(keyword: 'a') + end + CODE + end + it "fails if #has_sym?(*args) returns false" do expect { expect({ :b => "B" }).to have_key(:a)