lib/rspec/matchers/built_in/change.rb in rspec-expectations-3.7.0 vs lib/rspec/matchers/built_in/change.rb in rspec-expectations-3.8.0
- old
+ new
@@ -41,20 +41,17 @@
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?
+ 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
+ perform_change(event_proc) && !change_details.changed?
end
# @api private
# @return [String]
def failure_message
@@ -90,23 +87,35 @@
def change_details
@change_details ||= ChangeDetails.new(matcher_name, @receiver, @message, &@block)
end
+ def perform_change(event_proc)
+ @event_proc = event_proc
+ change_details.perform_change(event_proc) do |actual_before|
+ # pre-compute values derived from the `before` value before the
+ # mutation is applied, in case the specified mutation is mutation
+ # of a single object (rather than a changing what object a method
+ # returns). We need to cache these values before the `before` value
+ # they are based on potentially gets mutated.
+ @actual_before_description = description_of(actual_before)
+ end
+ 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}"
+ "is still #{@actual_before_description}"
end
def negative_failure_reason
return "was not given a block" unless Proc === @event_proc
- "did change from #{description_of change_details.actual_before} " \
+ "did change from #{@actual_before_description} " \
"to #{description_of change_details.actual_after}"
end
end
# Used to specify a relative change.
@@ -127,13 +136,11 @@
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)
+ @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 " \
@@ -171,25 +178,22 @@
@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?
+ perform_change(event_proc) && @change_details.changed? && @matches_before && matches_after?
end
# @private
def description
"change #{@change_details.value_representation} #{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 before_value_failure unless @matches_before
return did_not_change_failure unless @change_details.changed?
after_value_failure
end
# @private
@@ -197,22 +201,31 @@
true
end
private
- def matches_before?
- values_match?(@expected_before, @change_details.actual_before)
+ def perform_change(event_proc)
+ @event_proc = event_proc
+ @change_details.perform_change(event_proc) do |actual_before|
+ # pre-compute values derived from the `before` value before the
+ # mutation is applied, in case the specified mutation is mutation
+ # of a single object (rather than a changing what object a method
+ # returns). We need to cache these values before the `before` value
+ # they are based on potentially gets mutated.
+ @matches_before = values_match?(@expected_before, actual_before)
+ @actual_before_description = description_of(actual_before)
+ end
end
def matches_after?
values_match?(@expected_after, @change_details.actual_after)
end
def before_value_failure
"expected #{@change_details.value_representation} " \
"to have initially been #{description_of @expected_before}, " \
- "but was #{description_of @change_details.actual_before}"
+ "but was #{@actual_before_description}"
end
def after_value_failure
"expected #{@change_details.value_representation} " \
"to have changed to #{description_of @expected_after}, " \
@@ -224,11 +237,11 @@
"to have changed #{change_description}, but did not change"
end
def did_change_failure
"expected #{@change_details.value_representation} not to have changed, but " \
- "did change from #{description_of @change_details.actual_before} " \
+ "did change from #{@actual_before_description} " \
"to #{description_of @change_details.actual_after}"
end
def not_given_a_block_failure
"expected #{@change_details.value_representation} to have changed " \
@@ -258,20 +271,17 @@
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?
+ 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?
+ return before_value_failure unless @matches_before
did_change_failure
end
private
@@ -310,12 +320,24 @@
end
end
# @private
# Encapsulates the details of the before/after values.
+ #
+ # Note that this class exposes the `actual_after` value, to allow the
+ # matchers above to derive failure messages, etc from the value on demand
+ # as needed, but it intentionally does _not_ expose the `actual_before`
+ # value. Some usages of the `change` matcher mutate a specific object
+ # returned by the value proc, which means that failure message snippets,
+ # etc, which are derived from the `before` value may not be accurate if
+ # they are lazily computed as needed. We must pre-compute them before
+ # applying the change in the `expect` block. To ensure that all `change`
+ # matchers do that properly, we do not expose the `actual_before` value.
+ # Instead, matchers must pass a block to `perform_change`, which yields
+ # the `actual_before` value before applying the change.
class ChangeDetails
- attr_reader :actual_before, :actual_after
+ attr_reader :actual_after
def initialize(matcher_name, receiver=nil, message=nil, &block)
if receiver && !message
raise(
ArgumentError,
@@ -332,43 +354,60 @@
end
def value_representation
@value_representation ||=
if @message
- "##{@message}"
+ "`#{message_notation(@receiver, @message)}`"
elsif (value_block_snippet = extract_value_block_snippet)
"`#{value_block_snippet}`"
else
'result'
end
end
def perform_change(event_proc)
@actual_before = evaluate_value_proc
+ @before_hash = @actual_before.hash
+ yield @actual_before if block_given?
+
+ return false unless Proc === event_proc
event_proc.call
+
@actual_after = evaluate_value_proc
+ true
end
def changed?
- @actual_before != @actual_after
+ # Consider it changed if either:
+ #
+ # - The before/after values are unequal
+ # - The before/after values have different hash values
+ #
+ # The latter case specifically handles the case when the value proc
+ # returns the exact same object, but it has been mutated.
+ #
+ # Note that it is not sufficient to only check the hashes; it is
+ # possible for two values to be unequal (and of different classes)
+ # but to return the same hash value.
+ @actual_before != @actual_after || @before_hash != @actual_after.hash
end
def actual_delta
@actual_after - @actual_before
end
private
def evaluate_value_proc
- value_proc = @value_proc || lambda { @receiver.__send__(@message) }
+ @value_proc ? @value_proc.call : @receiver.__send__(@message)
+ end
- case val = value_proc.call
- when IO # enumerable, but we don't want to dup it.
- val
- when Enumerable, String
- val.dup
+ def message_notation(receiver, message)
+ case receiver
+ when Module
+ "#{receiver}.#{message}"
else
- val
+ "#{Support.class_of(receiver)}##{message}"
end
end
if RSpec::Support::RubyFeatures.ripper_supported?
def extract_value_block_snippet