lib/rspec/matchers/built_in/change.rb in rspec-expectations-3.0.0.beta1 vs lib/rspec/matchers/built_in/change.rb in rspec-expectations-3.0.0.beta2
- old
+ new
@@ -1,140 +1,257 @@
module RSpec
module Matchers
module BuiltIn
+ # Describes an expected mutation.
class Change
- def initialize(receiver=nil, message=nil, &block)
- @message = message
- @value_proc = block || lambda {receiver.__send__(message)}
- @expected_after = @expected_before = @minimum = @maximum = @expected_delta = nil
- @eval_before = @eval_after = false
+ include Composable
+
+ # 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
+ # 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
+
+ # 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
+
+ # Specifies the new value you expect.
+ def to(value)
+ ChangeToValue.new(@change_details, value)
+ end
+
+ # Specifies the original value.
+ def from(value)
+ ChangeFromValue.new(@change_details, value)
+ end
+
+ # @api private
def matches?(event_proc)
raise_block_syntax_error if block_given?
+ @change_details.perform_change(event_proc)
+ @change_details.changed?
+ end
- @actual_before = evaluate_value_proc
- event_proc.call
- @actual_after = evaluate_value_proc
+ # @api private
+ def failure_message
+ "expected #{@change_details.message} to have changed, but is still #{description_of @change_details.actual_before}"
+ end
- (!change_expected? || changed?) && matches_before? && matches_after? && matches_expected_delta? && matches_min? && matches_max?
+ # @api private
+ def failure_message_when_negated
+ "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
- alias == matches?
- def raise_block_syntax_error
- raise SyntaxError.new(<<-MESSAGE)
-block passed to should or should_not change must use {} instead of do/end
-MESSAGE
+ # @api private
+ def description
+ "change #{@change_details.message}"
end
- def evaluate_value_proc
- case val = @value_proc.call
- when Enumerable, String
- val.dup
- else
- val
- end
+ private
+
+ def initialize(receiver=nil, message=nil, &block)
+ @change_details = ChangeDetails.new(receiver, message, &block)
end
- def failure_message_for_should
- if @eval_before && !expected_matches_actual?(@expected_before, @actual_before)
- "#{message} should have initially been #{@expected_before.inspect}, but was #{@actual_before.inspect}"
- elsif @eval_after && !expected_matches_actual?(@expected_after, @actual_after)
- "#{message} should have been changed to #{failure_message_for_expected_after}, but is now #{@actual_after.inspect}"
- elsif @expected_delta
- "#{message} should have been changed by #{@expected_delta.inspect}, but was changed by #{actual_delta.inspect}"
- elsif @minimum
- "#{message} should have been changed by at least #{@minimum.inspect}, but was changed by #{actual_delta.inspect}"
- elsif @maximum
- "#{message} should have been changed by at most #{@maximum.inspect}, but was changed by #{actual_delta.inspect}"
- else
- "#{message} should have changed, but is still #{@actual_before.inspect}"
- end
+ def raise_block_syntax_error
+ raise SyntaxError,
+ "The block passed to the `change` matcher must use `{ ... }` instead of do/end"
end
+ end
- def actual_delta
- @actual_after - @actual_before
+ # Used to specify a relative change.
+ # @api private
+ class ChangeRelatively
+ include Composable
+
+ def initialize(change_details, expected_delta, relativity, &comparer)
+ @change_details = change_details
+ @expected_delta = expected_delta
+ @relativity = relativity
+ @comparer = comparer
end
- def failure_message_for_should_not
- "#{message} should not have changed, but did change from #{@actual_before.inspect} to #{@actual_after.inspect}"
+ def failure_message
+ "expected #{@change_details.message} to have changed #{@relativity.to_s.gsub("_", " ")} #{description_of @expected_delta}, " +
+ "but was changed by #{description_of @change_details.actual_delta}"
end
- def by(expected_delta)
- @expected_delta = expected_delta
- self
+ def matches?(event_proc)
+ @change_details.perform_change(event_proc)
+ @comparer.call(@change_details.actual_delta)
end
- def by_at_least(minimum)
- @minimum = minimum
- self
+ def does_not_match?(event_proc)
+ raise NotImplementedError, "`expect { }.not_to change { }.#{@relativity}()` is not supported"
end
- def by_at_most(maximum)
- @maximum = maximum
- self
+ # @api private
+ def description
+ "change #{@change_details.message} #{@relativity.to_s.gsub("_", " ")} #{description_of @expected_delta}"
end
+ end
- def to(to)
- @eval_after = true
- @expected_after = to
- self
+ # Base class for specifying a change from and/or to specific values.
+ # @api private
+ class SpecificValuesChange
+ include Composable
+ MATCH_ANYTHING = ::Object.ancestors.last
+
+ def initialize(change_details, from, to)
+ @change_details = change_details
+ @expected_before = from
+ @expected_after = to
end
- def from (before)
- @eval_before = true
- @expected_before = before
- self
+ def matches?(event_proc)
+ @change_details.perform_change(event_proc)
+ @change_details.changed? && matches_before? && matches_after?
end
def description
- "change ##{message}"
+ "change #{@change_details.message} #{change_description}"
end
- private
+ def failure_message
+ return before_value_failure unless matches_before?
+ return did_not_change_failure unless @change_details.changed?
+ after_value_failure
+ end
- def failure_message_for_expected_after
- if RSpec::Matchers.is_a_matcher?(@expected_after)
- @expected_after.description
- else
- @expected_after.inspect
+ 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
+ end
+
+ # Used to specify a change from a specific value
+ # (and, optionally, to a specific value).
+ # @api private
+ class ChangeFromValue < SpecificValuesChange
+ def initialize(change_details, expected_before)
+ @description_suffix = nil
+ super(change_details, expected_before, MATCH_ANYTHING)
+ end
+
+ def to(value)
+ @expected_after = value
+ @description_suffix = " to #{description_of value}"
+ self
+ end
+
+ def does_not_match?(event_proc)
+ if @description_suffix
+ raise NotImplementedError, "`expect { }.not_to change { }.to()` is not supported"
end
+
+ @change_details.perform_change(event_proc)
+ !@change_details.changed? && matches_before?
end
- def message
- @message || "result"
+ def failure_message_when_negated
+ return before_value_failure unless matches_before?
+ did_change_failure
end
- def change_expected?
- @expected_delta != 0
+ private
+
+ def change_description
+ "from #{description_of @expected_before}#{@description_suffix}"
end
+ end
- def changed?
- @actual_before != @actual_after
+ # Used to specify a change to a specific value
+ # (and, optionally, from a specific value).
+ # @api private
+ class ChangeToValue < SpecificValuesChange
+ def initialize(change_details, expected_after)
+ @description_suffix = nil
+ super(change_details, MATCH_ANYTHING, expected_after)
end
- def matches_before?
- @eval_before ? expected_matches_actual?(@expected_before, @actual_before) : true
+ def from(value)
+ @expected_before = value
+ @description_suffix = " from #{description_of value}"
+ self
end
- def matches_after?
- @eval_after ? expected_matches_actual?(@expected_after, @actual_after) : true
+ def does_not_match?(event_proc)
+ raise NotImplementedError, "`expect { }.not_to change { }.to()` is not supported"
end
- def matches_expected_delta?
- @expected_delta ? (@actual_before + @expected_delta == @actual_after) : true
+ private
+
+ def change_description
+ "to #{description_of @expected_after}#{@description_suffix}"
end
+ end
- def matches_min?
- @minimum ? (@actual_after - @actual_before >= @minimum) : true
+ # Encapsulates the details of the before/after values.
+ # @api private
+ class ChangeDetails
+ attr_reader :message, :actual_before, :actual_after
+
+ def initialize(receiver=nil, message=nil, &block)
+ @message = message ? "##{message}" : "result"
+ @value_proc = block || lambda { receiver.__send__(message) }
end
- def matches_max?
- @maximum ? (@actual_after - @actual_before <= @maximum) : true
+ def perform_change(event_proc)
+ @actual_before = evaluate_value_proc
+ event_proc.call
+ @actual_after = evaluate_value_proc
end
- def expected_matches_actual?(expected, actual)
- expected === actual || actual == expected
+ 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 Enumerable, String
+ val.dup
+ else
+ val
+ end
end
end
end
end
end