Find file
Fetching contributors…
Cannot retrieve contributors at this time
355 lines (298 sloc) 10.5 KB
module RSpec
module Matchers
module BuiltIn
# @api private
# Provides the implementation for `change`.
# Not intended to be instantiated directly.
class Change < BaseMatcher
# @api public
# Specifies the delta of the expected change.
def by(expected_delta)
ChangeRelatively.new(@change_details, expected_delta, :by) do |actual_delta|
values_match?(expected_delta, actual_delta)
end
end
# @api public
# Specifies a minimum delta of the expected change.
def by_at_least(minimum)
ChangeRelatively.new(@change_details, minimum, :by_at_least) do |actual_delta|
actual_delta >= minimum
end
end
# @api public
# Specifies a maximum delta of the expected change.
def by_at_most(maximum)
ChangeRelatively.new(@change_details, maximum, :by_at_most) do |actual_delta|
actual_delta <= maximum
end
end
# @api public
# Specifies the new value you expect.
def to(value)
ChangeToValue.new(@change_details, value)
end
# @api public
# Specifies the original value.
def from(value)
ChangeFromValue.new(@change_details, value)
end
# @private
def matches?(event_proc)
@event_proc = event_proc
return false unless Proc === event_proc
raise_block_syntax_error if block_given?
@change_details.perform_change(event_proc)
@change_details.changed?
end
def does_not_match?(event_proc)
raise_block_syntax_error if block_given?
!matches?(event_proc) && Proc === event_proc
end
# @api private
# @return [String]
def failure_message
"expected #{@change_details.message} to have changed, " \
"but #{positive_failure_reason}"
end
# @api private
# @return [String]
def failure_message_when_negated
"expected #{@change_details.message} not to have changed, " \
"but #{negative_failure_reason}"
end
# @api private
# @return [String]
def description
"change #{@change_details.message}"
end
# @private
def supports_block_expectations?
true
end
private
def initialize(receiver=nil, message=nil, &block)
@change_details = ChangeDetails.new(receiver, message, &block)
end
def raise_block_syntax_error
raise SyntaxError, "Block not received by the `change` matcher. " \
"Perhaps you want to use `{ ... }` instead of do/end?"
end
def positive_failure_reason
return "was not given a block" unless Proc === @event_proc
"is still #{description_of @change_details.actual_before}"
end
def negative_failure_reason
return "was not given a block" unless Proc === @event_proc
"did change from #{description_of @change_details.actual_before} " \
"to #{description_of @change_details.actual_after}"
end
end
# Used to specify a relative change.
# @api private
class ChangeRelatively < BaseMatcher
def initialize(change_details, expected_delta, relativity, &comparer)
@change_details = change_details
@expected_delta = expected_delta
@relativity = relativity
@comparer = comparer
end
# @private
def failure_message
"expected #{@change_details.message} to have changed " \
"#{@relativity.to_s.tr('_', ' ')} " \
"#{description_of @expected_delta}, but #{failure_reason}"
end
# @private
def matches?(event_proc)
@event_proc = event_proc
return false unless Proc === event_proc
@change_details.perform_change(event_proc)
@comparer.call(@change_details.actual_delta)
end
# @private
def does_not_match?(_event_proc)
raise NotImplementedError, "`expect { }.not_to change " \
"{ }.#{@relativity}()` is not supported"
end
# @private
def description
"change #{@change_details.message} " \
"#{@relativity.to_s.tr('_', ' ')} #{description_of @expected_delta}"
end
# @private
def supports_block_expectations?
true
end
private
def failure_reason
return "was not given a block" unless Proc === @event_proc
"was changed by #{description_of @change_details.actual_delta}"
end
end
# @api private
# Base class for specifying a change from and/or to specific values.
class SpecificValuesChange < BaseMatcher
# @private
MATCH_ANYTHING = ::Object.ancestors.last
def initialize(change_details, from, to)
@change_details = change_details
@expected_before = from
@expected_after = to
end
# @private
def matches?(event_proc)
@event_proc = event_proc
return false unless Proc === event_proc
@change_details.perform_change(event_proc)
@change_details.changed? && matches_before? && matches_after?
end
# @private
def description
"change #{@change_details.message} #{change_description}"
end
# @private
def failure_message
return not_given_a_block_failure unless Proc === @event_proc
return before_value_failure unless matches_before?
return did_not_change_failure unless @change_details.changed?
after_value_failure
end
# @private
def supports_block_expectations?
true
end
private
def matches_before?
values_match?(@expected_before, @change_details.actual_before)
end
def matches_after?
values_match?(@expected_after, @change_details.actual_after)
end
def before_value_failure
"expected #{@change_details.message} " \
"to have initially been #{description_of @expected_before}, " \
"but was #{description_of @change_details.actual_before}"
end
def after_value_failure
"expected #{@change_details.message} " \
"to have changed to #{description_of @expected_after}, " \
"but is now #{description_of @change_details.actual_after}"
end
def did_not_change_failure
"expected #{@change_details.message} " \
"to have changed #{change_description}, but did not change"
end
def did_change_failure
"expected #{@change_details.message} not to have changed, but " \
"did change from #{description_of @change_details.actual_before} " \
"to #{description_of @change_details.actual_after}"
end
def not_given_a_block_failure
"expected #{@change_details.message} to have changed " \
"#{change_description}, but was not given a block"
end
end
# @api private
# Used to specify a change from a specific value
# (and, optionally, to a specific value).
class ChangeFromValue < SpecificValuesChange
def initialize(change_details, expected_before)
@description_suffix = nil
super(change_details, expected_before, MATCH_ANYTHING)
end
# @api public
# Specifies the new value you expect.
def to(value)
@expected_after = value
@description_suffix = " to #{description_of value}"
self
end
# @private
def does_not_match?(event_proc)
if @description_suffix
raise NotImplementedError, "`expect { }.not_to change { }.to()` " \
"is not supported"
end
@event_proc = event_proc
return false unless Proc === event_proc
@change_details.perform_change(event_proc)
!@change_details.changed? && matches_before?
end
# @private
def failure_message_when_negated
return not_given_a_block_failure unless Proc === @event_proc
return before_value_failure unless matches_before?
did_change_failure
end
private
def change_description
"from #{description_of @expected_before}#{@description_suffix}"
end
end
# @api private
# Used to specify a change to a specific value
# (and, optionally, from a specific value).
class ChangeToValue < SpecificValuesChange
def initialize(change_details, expected_after)
@description_suffix = nil
super(change_details, MATCH_ANYTHING, expected_after)
end
# @api public
# Specifies the original value.
def from(value)
@expected_before = value
@description_suffix = " from #{description_of value}"
self
end
# @private
def does_not_match?(_event_proc)
raise NotImplementedError, "`expect { }.not_to change { }.to()` " \
"is not supported"
end
private
def change_description
"to #{description_of @expected_after}#{@description_suffix}"
end
end
# @private
# Encapsulates the details of the before/after values.
class ChangeDetails
attr_reader :message, :actual_before, :actual_after
def initialize(receiver=nil, message=nil, &block)
if receiver && !message
raise(
ArgumentError,
"`change` requires either an object and message " \
"(`change(obj, :msg)`) or a block (`change { }`). " \
"You passed an object but no message."
)
end
@message = message ? "##{message}" : "result"
@value_proc = block || lambda { receiver.__send__(message) }
end
def perform_change(event_proc)
@actual_before = evaluate_value_proc
event_proc.call
@actual_after = evaluate_value_proc
end
def changed?
@actual_before != @actual_after
end
def actual_delta
@actual_after - @actual_before
end
private
def evaluate_value_proc
case val = @value_proc.call
when IO # enumerable, but we don't want to dup it.
val
when Enumerable, String
val.dup
else
val
end
end
end
end
end
end