Skip to content

Commit

Permalink
Merge pull request #1197 from marcandre/refactor_same_output
Browse files Browse the repository at this point in the history
Factorize common DynamicPredicate base.
  • Loading branch information
JonRowe authored Aug 27, 2020
2 parents 236d55f + c028a9a commit 8c68e05
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 160 deletions.
2 changes: 1 addition & 1 deletion lib/rspec/matchers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -951,7 +951,7 @@ def self.configuration

private

BE_PREDICATE_REGEX = /^(be_(?:an?_)?)(.*)/
BE_PREDICATE_REGEX = /^(?:be_(?:an?_)?)(.*)/
HAS_REGEX = /^(?:have_)(.*)/
DYNAMIC_MATCHER_REGEX = Regexp.union(BE_PREDICATE_REGEX, HAS_REGEX)

Expand Down
3 changes: 2 additions & 1 deletion lib/rspec/matchers/built_in.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ module BuiltIn
autoload :Be, 'rspec/matchers/built_in/be'
autoload :BeComparedTo, 'rspec/matchers/built_in/be'
autoload :BeFalsey, 'rspec/matchers/built_in/be'
autoload :BeHelpers, 'rspec/matchers/built_in/be'
autoload :BeNil, 'rspec/matchers/built_in/be'
autoload :BePredicate, 'rspec/matchers/built_in/be'
autoload :BePredicate, 'rspec/matchers/built_in/has'
autoload :BeTruthy, 'rspec/matchers/built_in/be'
autoload :BeWithin, 'rspec/matchers/built_in/be_within'
autoload :Change, 'rspec/matchers/built_in/change'
Expand Down
133 changes: 0 additions & 133 deletions lib/rspec/matchers/built_in/be.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,139 +186,6 @@ def perform_match(actual)
@actual.__send__ @operator, @expected
end
end

# @api private
# Provides the implementation of `be_<predicate>`.
# Not intended to be instantiated directly.
class BePredicate < BaseMatcher
include BeHelpers

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)
@actual = actual
@block ||= block
predicate_accessible? && predicate_matches?
end

def does_not_match?(actual, &block)
@actual = actual
@block ||= block
predicate_accessible? && !predicate_matches?
end

# @api private
# @return [String]
def failure_message
failure_message_expecting(true)
end

# @api private
# @return [String]
def failure_message_when_negated
failure_message_expecting(false)
end

# @api private
# @return [String]
def description
"#{prefix_to_sentence}#{expected_to_sentence}#{args_to_sentence}"
end

private

def predicate_accessible?
actual.respond_to?(predicate) || actual.respond_to?(present_tense_predicate)
end

# support 1.8.7, evaluate once at load time for performance
if String === methods.first
# :nocov:
def private_predicate?
@actual.private_methods.include? predicate.to_s
end
# :nocov:
else
def private_predicate?
@actual.private_methods.include? predicate
end
end

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
:"#{@expected}?"
end

def present_tense_predicate
:"#{@expected}s?"
end

def parse_expected(expected)
@prefix, expected = prefix_and_expected(expected)
expected
end

def prefix_and_expected(symbol)
Matchers::BE_PREDICATE_REGEX.match(symbol.to_s).captures.compact
end

def prefix_to_sentence
EnglishPhrasing.split_words(@prefix)
end

def failure_message_expecting(value)
validity_message ||
"expected `#{actual_formatted}.#{predicate}#{args_to_s}` to return #{value}, got #{description_of @predicate_matches}"
end

def validity_message
return nil if predicate_accessible?

msg = "expected #{actual_formatted} to respond to `#{predicate}`".dup

if private_predicate?
msg << " but `#{predicate}` is a private method"
elsif predicate == :true?
msg << " or perhaps you meant `be true` or `be_truthy`"
elsif predicate == :false?
msg << " or perhaps you meant `be false` or `be_falsey`"
end

msg
end
end
end
end
end
129 changes: 104 additions & 25 deletions lib/rspec/matchers/built_in/has.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ module RSpec
module Matchers
module BuiltIn
# @api private
# Provides the implementation for `has_<predicate>`.
# Not intended to be instantiated directly.
class Has < BaseMatcher
# Provides the implementation for dynamic predicate matchers.
# Not intended to be inherited directly.
class DynamicPredicate < BaseMatcher
include BeHelpers

if RSpec::Support::RubyFeatures.kw_args_supported?
binding.eval(<<-CODE, __FILE__, __LINE__)
def initialize(method_name, *args, **kwargs, &block)
Expand Down Expand Up @@ -34,25 +36,25 @@ def does_not_match?(actual, &block)
# @api private
# @return [String]
def failure_message
validity_message || "expected ##{predicate}#{failure_message_args_description} to return true, got false"
failure_message_expecting(true)
end

# @api private
# @return [String]
def failure_message_when_negated
validity_message || "expected ##{predicate}#{failure_message_args_description} to return false, got true"
failure_message_expecting(false)
end

# @api private
# @return [String]
def description
[method_description, args_description].compact.join(' ')
"#{method_description}#{args_to_sentence}"
end

private

def predicate_accessible?
!private_predicate? && predicate_exists?
@actual.respond_to? predicate
end

# support 1.8.7, evaluate once at load time for performance
Expand All @@ -68,38 +70,87 @@ def private_predicate?
end
end

def predicate_exists?
@actual.respond_to? predicate
end

if RSpec::Support::RubyFeatures.kw_args_supported?
binding.eval(<<-CODE, __FILE__, __LINE__)
def predicate_matches?
def predicate_result
if @kwargs.empty?
@actual.__send__(predicate, *@args, &@block)
@predicate_result = actual.__send__(predicate_method_name, *@args, &@block)
else
@actual.__send__(predicate, *@args, **@kwargs, &@block)
@predicate_result = actual.__send__(predicate_method_name, *@args, **@kwargs, &@block)
end
end
CODE
else
def predicate_matches?
@actual.__send__(predicate, *@args, &@block)
def predicate_result
@predicate_result = actual.__send__(predicate_method_name, *@args, &@block)
end
end

def predicate
def predicate_method_name
predicate
end

def predicate_matches?
!!predicate_result
end

def root
# On 1.9, there appears to be a bug where String#match can return `false`
# rather than the match data object. Changing to Regex#match appears to
# work around this bug. For an example of this bug, see:
# https://travis-ci.org/rspec/rspec-expectations/jobs/27549635
@predicate ||= :"has_#{Matchers::HAS_REGEX.match(@method_name.to_s).captures.first}?"
self.class::REGEX.match(@method_name.to_s).captures.first
end

def method_description
@method_name.to_s.tr('_', ' ')
EnglishPhrasing.split_words(@method_name)
end

def failure_message_expecting(value)
validity_message ||
"expected `#{actual_formatted}.#{predicate}#{args_to_s}` to return #{value}, got #{description_of @predicate_result}"
end

def validity_message
return nil if predicate_accessible?

"expected #{actual_formatted} to respond to `#{predicate}`#{failure_to_respond_explanation}"
end

def failure_to_respond_explanation
if private_predicate?
" but `#{predicate}` is a private method"
end
end
end

# @api private
# Provides the implementation for `has_<predicate>`.
# Not intended to be instantiated directly.
class Has < DynamicPredicate
# :nodoc:
REGEX = Matchers::HAS_REGEX

# @api private
# @return [String]
def failure_message
validity_message || "expected ##{predicate}#{failure_message_args_description} to return true, got false"
end

# @api private
# @return [String]
def failure_message_when_negated
validity_message || "expected ##{predicate}#{failure_message_args_description} to return false, got true"
end

# @api private
# @return [String]
def description
[method_description, args_description].compact.join(' ')
end

private

def args_description
return nil if @args.empty?
@args.map { |arg| RSpec::Support::ObjectFormatter.format(arg) }.join(', ')
Expand All @@ -110,12 +161,40 @@ def failure_message_args_description
"(#{desc})" if desc
end

def validity_message
if private_predicate?
"expected #{@actual} to respond to `#{predicate}` but `#{predicate}` is a private method"
elsif !predicate_exists?
"expected #{@actual} to respond to `#{predicate}`"
end
def predicate
@predicate ||= :"has_#{root}?"
end
end

# @api private
# Provides the implementation of `be_<predicate>`.
# Not intended to be instantiated directly.
class BePredicate < DynamicPredicate
# :nodoc:
REGEX = Matchers::BE_PREDICATE_REGEX
private
def predicate
@predicate ||= :"#{root}?"
end

def predicate_method_name
actual.respond_to?(predicate) ? predicate : present_tense_predicate
end

def failure_to_respond_explanation
super || if predicate == :true?
" or perhaps you meant `be true` or `be_truthy`"
elsif predicate == :false?
" or perhaps you meant `be false` or `be_falsey`"
end
end

def predicate_accessible?
super || actual.respond_to?(present_tense_predicate)
end

def present_tense_predicate
:"#{root}s?"
end
end
end
Expand Down

0 comments on commit 8c68e05

Please sign in to comment.