From c028a9ae062a0262b33b8169631dc2d82be90715 Mon Sep 17 00:00:00 2001 From: Marc-Andre Lafortune Date: Thu, 18 Jun 2020 09:30:34 -0400 Subject: [PATCH] Factorize common DynamicPredicate base. Output is kept the same temporarily --- lib/rspec/matchers.rb | 2 +- lib/rspec/matchers/built_in.rb | 3 +- lib/rspec/matchers/built_in/be.rb | 133 ----------------------------- lib/rspec/matchers/built_in/has.rb | 129 ++++++++++++++++++++++------ 4 files changed, 107 insertions(+), 160 deletions(-) diff --git a/lib/rspec/matchers.rb b/lib/rspec/matchers.rb index da6583554..d39b18268 100644 --- a/lib/rspec/matchers.rb +++ b/lib/rspec/matchers.rb @@ -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) diff --git a/lib/rspec/matchers/built_in.rb b/lib/rspec/matchers/built_in.rb index 7f184a00a..e6237ff08 100644 --- a/lib/rspec/matchers/built_in.rb +++ b/lib/rspec/matchers/built_in.rb @@ -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' diff --git a/lib/rspec/matchers/built_in/be.rb b/lib/rspec/matchers/built_in/be.rb index ebe9ae687..40d401718 100644 --- a/lib/rspec/matchers/built_in/be.rb +++ b/lib/rspec/matchers/built_in/be.rb @@ -186,139 +186,6 @@ def perform_match(actual) @actual.__send__ @operator, @expected end end - - # @api private - # Provides the implementation of `be_`. - # 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 diff --git a/lib/rspec/matchers/built_in/has.rb b/lib/rspec/matchers/built_in/has.rb index cc34fd3b7..d002d3381 100644 --- a/lib/rspec/matchers/built_in/has.rb +++ b/lib/rspec/matchers/built_in/has.rb @@ -2,9 +2,11 @@ module RSpec module Matchers module BuiltIn # @api private - # Provides the implementation for `has_`. - # 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) @@ -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 @@ -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_`. + # 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(', ') @@ -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_`. + # 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