Skip to content

Commit

Permalink
[expectations] Merge pull request rspec/rspec-expectations#1187 from …
Browse files Browse the repository at this point in the history
…rspec/remove-kw-args-warning-from-has-matcher-without-rspec-support

Ensure that has_<n> matcher works silently with keyword arguments

---
This commit was imported from rspec/rspec-expectations@9a2f90c.
  • Loading branch information
JonRowe committed Jun 14, 2020
1 parent 0fffbff commit b0ce75b
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 24 deletions.
33 changes: 24 additions & 9 deletions rspec-expectations/lib/rspec/matchers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
38 changes: 31 additions & 7 deletions rspec-expectations/lib/rspec/matchers/built_in/be.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
28 changes: 24 additions & 4 deletions rspec-expectations/lib/rspec/matchers/built_in/has.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@ module BuiltIn
# Provides the implementation for `has_<predicate>`.
# 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
Expand Down Expand Up @@ -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
Expand Down
31 changes: 27 additions & 4 deletions rspec-expectations/spec/rspec/matchers/built_in/be_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions rspec-expectations/spec/rspec/matchers/built_in/has_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit b0ce75b

Please sign in to comment.