rspec-expectations/upstream/lib/rspec/matchers/dsl.rb in opal-rspec-0.8.0 vs rspec-expectations/upstream/lib/rspec/matchers/dsl.rb in opal-rspec-1.0.0.alpha1
- old
+ new
@@ -1,18 +1,103 @@
+RSpec::Support.require_rspec_support "with_keywords_when_needed"
+
module RSpec
module Matchers
# Defines the custom matcher DSL.
module DSL
+ # Defines a matcher alias. The returned matcher's `description` will be overriden
+ # to reflect the phrasing of the new name, which will be used in failure messages
+ # when passed as an argument to another matcher in a composed matcher expression.
+ #
+ # @example
+ # RSpec::Matchers.alias_matcher :a_list_that_sums_to, :sum_to
+ # sum_to(3).description # => "sum to 3"
+ # a_list_that_sums_to(3).description # => "a list that sums to 3"
+ #
+ # @example
+ # RSpec::Matchers.alias_matcher :a_list_sorted_by, :be_sorted_by do |description|
+ # description.sub("be sorted by", "a list sorted by")
+ # end
+ #
+ # be_sorted_by(:age).description # => "be sorted by age"
+ # a_list_sorted_by(:age).description # => "a list sorted by age"
+ #
+ # @param new_name [Symbol] the new name for the matcher
+ # @param old_name [Symbol] the original name for the matcher
+ # @param options [Hash] options for the aliased matcher
+ # @option options [Class] :klass the ruby class to use as the decorator. (Not normally used).
+ # @yield [String] optional block that, when given, is used to define the overriden
+ # logic. The yielded arg is the original description or failure message. If no
+ # block is provided, a default override is used based on the old and new names.
+ # @see RSpec::Matchers
+ def alias_matcher(new_name, old_name, options={}, &description_override)
+ description_override ||= lambda do |old_desc|
+ old_desc.gsub(EnglishPhrasing.split_words(old_name), EnglishPhrasing.split_words(new_name))
+ end
+ klass = options.fetch(:klass) { AliasedMatcher }
+
+ define_method(new_name) do |*args, &block|
+ matcher = __send__(old_name, *args, &block)
+ matcher.matcher_name = new_name if matcher.respond_to?(:matcher_name=)
+ klass.new(matcher, description_override)
+ end
+ end
+
+ # Defines a negated matcher. The returned matcher's `description` and `failure_message`
+ # will be overriden to reflect the phrasing of the new name, and the match logic will
+ # be based on the original matcher but negated.
+ #
+ # @example
+ # RSpec::Matchers.define_negated_matcher :exclude, :include
+ # include(1, 2).description # => "include 1 and 2"
+ # exclude(1, 2).description # => "exclude 1 and 2"
+ #
+ # @param negated_name [Symbol] the name for the negated matcher
+ # @param base_name [Symbol] the name of the original matcher that will be negated
+ # @yield [String] optional block that, when given, is used to define the overriden
+ # logic. The yielded arg is the original description or failure message. If no
+ # block is provided, a default override is used based on the old and new names.
+ # @see RSpec::Matchers
+ def define_negated_matcher(negated_name, base_name, &description_override)
+ alias_matcher(negated_name, base_name, :klass => AliasedNegatedMatcher, &description_override)
+ end
+
# Defines a custom matcher.
+ #
+ # @param name [Symbol] the name for the matcher
+ # @yield [Object] block that is used to define the matcher.
+ # The block is evaluated in the context of your custom matcher class.
+ # When args are passed to your matcher, they will be yielded here,
+ # usually representing the expected value(s).
# @see RSpec::Matchers
def define(name, &declarations)
- define_method name do |*expected|
- RSpec::Matchers::DSL::Matcher.new(name, declarations, self, *expected)
+ warn_about_block_args(name, declarations)
+ define_method name do |*expected, &block_arg|
+ RSpec::Matchers::DSL::Matcher.new(name, declarations, self, *expected, &block_arg)
end
end
alias_method :matcher, :define
+ private
+
+ if Proc.method_defined?(:parameters)
+ def warn_about_block_args(name, declarations)
+ declarations.parameters.each do |type, arg_name|
+ next unless type == :block
+ RSpec.warning("Your `#{name}` custom matcher receives a block argument (`#{arg_name}`), " \
+ "but due to limitations in ruby, RSpec cannot provide the block. Instead, " \
+ "use the `block_arg` method to access the block")
+ end
+ end
+ else
+ # :nocov:
+ def warn_about_block_args(*)
+ # There's no way to detect block params on 1.8 since the method reflection APIs don't expose it
+ end
+ # :nocov:
+ end
+
RSpec.configure { |c| c.extend self } if RSpec.respond_to?(:configure)
# Contains the methods that are available from within the
# `RSpec::Matchers.define` DSL for creating custom matchers.
module Macros
@@ -33,32 +118,57 @@
# expect(4).to be_even # passes
# expect(3).not_to be_even # passes
# expect(3).to be_even # fails
# expect(4).not_to be_even # fails
#
+ # By default the match block will swallow expectation errors (e.g.
+ # caused by using an expectation such as `expect(1).to eq 2`), if you
+ # wish to allow these to bubble up, pass in the option
+ # `:notify_expectation_failures => true`.
+ #
+ # @param [Hash] options for defining the behavior of the match block.
# @yield [Object] actual the actual value (i.e. the value wrapped by `expect`)
- def match(&match_block)
+ def match(options={}, &match_block)
define_user_override(:matches?, match_block) do |actual|
- begin
- @actual = actual
- super(*actual_arg_for(match_block))
- rescue RSpec::Expectations::ExpectationNotMetError
- false
+ @actual = actual
+ RSpec::Support.with_failure_notifier(RAISE_NOTIFIER) do
+ begin
+ super(*actual_arg_for(match_block))
+ rescue RSpec::Expectations::ExpectationNotMetError
+ raise if options[:notify_expectation_failures]
+ false
+ end
end
end
end
+ # @private
+ RAISE_NOTIFIER = Proc.new { |err, _opts| raise err }
+
# Use this to define the block for a negative expectation (`expect(...).not_to`)
# when the positive and negative forms require different handling. This
# is rarely necessary, but can be helpful, for example, when specifying
# asynchronous processes that require different timeouts.
#
+ # By default the match block will swallow expectation errors (e.g.
+ # caused by using an expectation such as `expect(1).to eq 2`), if you
+ # wish to allow these to bubble up, pass in the option
+ # `:notify_expectation_failures => true`.
+ #
+ # @param [Hash] options for defining the behavior of the match block.
# @yield [Object] actual the actual value (i.e. the value wrapped by `expect`)
- def match_when_negated(&match_block)
+ def match_when_negated(options={}, &match_block)
define_user_override(:does_not_match?, match_block) do |actual|
- @actual = actual
- super(*actual_arg_for(match_block))
+ begin
+ @actual = actual
+ RSpec::Support.with_failure_notifier(RAISE_NOTIFIER) do
+ super(*actual_arg_for(match_block))
+ end
+ rescue RSpec::Expectations::ExpectationNotMetError
+ raise if options[:notify_expectation_failures]
+ false
+ end
end
end
# Use this instead of `match` when the block will raise an exception
# rather than returning false to indicate a failure.
@@ -163,10 +273,16 @@
# for you. If the method is invoked and the
# `include_chain_clauses_in_custom_matcher_descriptions` config option
# hash been enabled, the chained method name and args will be added to the
# default description and failure message.
#
+ # In the common case where you just want the chained method to store some
+ # value(s) for later use (e.g. in `match`), you can provide one or more
+ # attribute names instead of a block; the chained method will store its
+ # arguments in instance variables with those names, and the values will
+ # be exposed via getters.
+ #
# @example
#
# RSpec::Matchers.define :have_errors_on do |key|
# chain :with do |message|
# @message = message
@@ -176,34 +292,55 @@
# actual.errors[key] == @message
# end
# end
#
# expect(minor).to have_errors_on(:age).with("Not old enough to participate")
- def chain(name, &definition)
- define_user_override(name, definition) do |*args, &block|
+ def chain(method_name, *attr_names, &definition)
+ unless block_given? ^ attr_names.any?
+ raise ArgumentError, "You must pass either a block or some attribute names (but not both) to `chain`."
+ end
+
+ definition = assign_attributes(attr_names) if attr_names.any?
+
+ define_user_override(method_name, definition) do |*args, &block|
super(*args, &block)
- @chained_method_clauses.push([name, args])
+ @chained_method_clauses.push([method_name, args])
self
end
end
+ def assign_attributes(attr_names)
+ attr_reader(*attr_names)
+ private(*attr_names)
+
+ lambda do |*attr_values|
+ attr_names.zip(attr_values) do |attr_name, attr_value|
+ instance_variable_set(:"@#{attr_name}", attr_value)
+ end
+ end
+ end
+
+ # assign_attributes isn't defined in the private section below because
+ # that makes MRI 1.9.2 emit a warning about private attributes.
+ private :assign_attributes
+
private
# Does the following:
#
- # - Defines the named method usign a user-provided block
+ # - Defines the named method using a user-provided block
# in @user_method_defs, which is included as an ancestor
# in the singleton class in which we eval the `define` block.
# - Defines an overriden definition for the same method
# usign the provided `our_def` block.
# - Provides a default `our_def` block for the common case
# of needing to call the user's definition with `@actual`
# as an arg, but only if their block's arity can handle it.
#
# This compiles the user block into an actual method, allowing
# them to use normal method constructs like `return`
- # (e.g. for a early guard statement), while allowing us to define
+ # (e.g. for an early guard statement), while allowing us to define
# an override that can provide the wrapped handling
# (e.g. assigning `@actual`, rescueing errors, etc) and
# can `super` to the user's definition.
def define_user_override(method_name, user_def, &our_def)
@user_method_defs.__send__(:define_method, method_name, &user_def)
@@ -253,19 +390,25 @@
false
end
# The default description.
def description
- "#{name_to_sentence}#{to_sentence expected}#{chained_method_clause_sentences}"
+ english_name = EnglishPhrasing.split_words(name)
+ expected_list = EnglishPhrasing.list(expected)
+ "#{english_name}#{expected_list}#{chained_method_clause_sentences}"
end
# Matchers do not support block expectations by default. You
# must opt-in.
def supports_block_expectations?
false
end
+ def supports_value_expectations?
+ true
+ end
+
# Most matchers do not expect call stack jumps.
def expects_call_stack_jump?
false
end
@@ -273,11 +416,13 @@
def chained_method_clause_sentences
return '' unless Expectations.configuration.include_chain_clauses_in_custom_matcher_descriptions?
@chained_method_clauses.map do |(method_name, method_args)|
- " #{split_words(method_name)}#{to_sentence(method_args)}"
+ english_name = EnglishPhrasing.split_words(method_name)
+ arg_list = EnglishPhrasing.list(method_args)
+ " #{english_name}#{arg_list}"
end.join
end
end
# The class used for custom matchers. The block passed to
@@ -289,13 +434,10 @@
include DefaultImplementations
# Allows expectation expressions to be used in the match block.
include RSpec::Matchers
- # Converts matcher name and expected args to an English expresion.
- include RSpec::Matchers::Pretty
-
# Supports the matcher composability features of RSpec 3+.
include Composable
# Makes the macro methods available to an `RSpec::Matchers.define` block.
extend Macros
@@ -307,23 +449,31 @@
# Exposes the exception raised during the matching by `match_unless_raises`.
# Could be useful to extract details for a failure message.
attr_reader :rescued_exception
+ # The block parameter used in the expectation
+ attr_reader :block_arg
+
+ # The name of the matcher.
+ attr_reader :name
+
# @api private
- def initialize(name, declarations, matcher_execution_context, *expected)
+ def initialize(name, declarations, matcher_execution_context, *expected, &block_arg)
@name = name
@actual = nil
@expected_as_array = expected
@matcher_execution_context = matcher_execution_context
@chained_method_clauses = []
+ @block_arg = block_arg
- class << self
+ klass = class << self
# See `Macros#define_user_override` above, for an explanation.
include(@user_method_defs = Module.new)
self
- end.class_exec(*expected, &declarations)
+ end
+ RSpec::Support::WithKeywordsWhenNeeded.class_exec(klass, *expected, &declarations)
end
# Provides the expected value. This will return an array if
# multiple arguments were passed to the matcher; otherwise it
# will return a single value.
@@ -355,15 +505,17 @@
# Also, supports getting a method object for such methods.
def respond_to_missing?(method, include_private=false)
super || @matcher_execution_context.respond_to?(method, include_private)
end
else # for 1.8.7
+ # :nocov:
# Indicates that this matcher responds to messages
# from the `@matcher_execution_context` as well.
def respond_to?(method, include_private=false)
super || @matcher_execution_context.respond_to?(method, include_private)
end
+ # :nocov:
end
private
def actual_arg_for(block)
@@ -381,11 +533,12 @@
@matcher_execution_context.__send__ method, *args, &block
else
super(method, *args, &block)
end
end
+ # The method_missing method should be refactored to pass kw args in RSpec 4
+ # then this can be removed
+ ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
end
end
end
end
-
-RSpec::Matchers.extend RSpec::Matchers::DSL