require 'hardmock/utils'
module Hardmock
class Expectation
include Utils
attr_reader :block_value
def initialize(options) #:nodoc:
@options = options
end
def apply_method_call(mock,mname,args,block) #:nodoc:
unless @options[:mock].equal?(mock)
raise anger("Wrong object", mock,mname,args)
end
unless @options[:method] == mname
raise anger("Wrong method",mock,mname,args)
end
# Tester-defined block to invoke at method-call-time:
expectation_block = @options[:block]
expected_args = @options[:arguments]
# if we have a block, we can skip the argument check if none were specified
unless (expected_args.nil? || expected_args.empty?) && expectation_block && !@options[:suppress_arguments_to_block]
unless expected_args == args
raise anger("Wrong arguments",mock,mname,args)
end
end
relayed_args = args.dup
if block
if expectation_block.nil?
# Can't handle a runtime block without an expectation block
raise ExpectationError.new("Unexpected block provided to #{to_s}")
else
# Runtime blocks are passed as final argument to the expectation block
unless @options[:suppress_arguments_to_block]
relayed_args << block
else
# Arguments suppressed; send only the block
relayed_args = [block]
end
end
end
# Run the expectation block:
@block_value = expectation_block.call(*relayed_args) if expectation_block
raise @options[:raises] unless @options[:raises].nil?
return_value = @options[:returns]
if return_value.nil?
return @block_value
else
return return_value
end
end
# Set the return value for an expected method call.
# Eg,
# @cash_machine.expects.withdraw(20,:dollars).returns(20.00)
def returns(val)
@options[:returns] = val
self
end
alias_method :and_return, :returns
# Set the arguments for an expected method call.
# Eg,
# @cash_machine.expects.deposit.with(20, "dollars").returns(:balance => "20")
def with(*args)
@options[:arguments] = args
self
end
# Rig an expected method to raise an exception when the mock is invoked.
#
# Eg,
# @cash_machine.expects.withdraw(20,:dollars).raises "Insufficient funds"
#
# The argument can be:
# * an Exception -- will be used directly
# * a String -- will be used as the message for a RuntimeError
# * nothing -- RuntimeError.new("An Error") will be raised
def raises(err=nil)
case err
when Exception
@options[:raises] = err
when String
@options[:raises] = RuntimeError.new(err)
else
@options[:raises] = RuntimeError.new("An Error")
end
self
end
# Convenience method: assumes +block_value+ is set, and is set to a Proc
# (or anything that responds to 'call')
#
# light_event = @traffic_light.trap.subscribe(:light_changes)
#
# # This code will meet the expectation:
# @traffic_light.subscribe :light_changes do |color|
# puts color
# end
#
# The color-handling block is now stored in light_event.block_value
#
# The block can be invoked like this:
#
# light_event.trigger :red
#
# See Mock#trap and Mock#expects for information on using expectation objects
# after they are set.
#
def trigger(*block_arguments)
unless block_value
raise ExpectationError.new("No block value is currently set for expectation #{to_s}")
end
unless block_value.respond_to?(:call)
raise ExpectationError.new("Can't apply trigger to #{block_value} for expectation #{to_s}")
end
block_value.call *block_arguments
end
# Used when an expected method accepts a block at runtime.
# When the expected method is invoked, the block passed to
# that method will be invoked as well.
#
# NOTE: ExpectationError will be thrown upon running the expected method
# if the arguments you set up in +yields+ do not properly match up with
# the actual block that ends up getting passed.
#
# == Examples
# Single invocation: The block passed to +lock_down+ gets invoked
# once with no arguments:
#
# @safe_zone.expects.lock_down.yields
#
# # (works on code that looks like:)
# @safe_zone.lock_down do
# # ... this block invoked once
# end
#
# Multi-parameter blocks: The block passed to +each_item+ gets
# invoked twice, with :item1 the first time, and with
# :item2 the second time:
#
# @fruit_basket.expects.each_with_index.yields [:apple,1], [:orange,2]
#
# # (works on code that looks like:)
# @fruit_basket.each_with_index do |fruit,index|
# # ... this block invoked with fruit=:apple, index=1,
# # ... and then with fruit=:orange, index=2
# end
#
# Arrays can be passed as arguments too... if the block
# takes a single argument and you want to pass a series of arrays into it,
# that will work as well:
#
# @list_provider.expects.each_list.yields [1,2,3], [4,5,6]
#
# # (works on code that looks like:)
# @list_provider.each_list do |list|
# # ... list is [1,2,3] the first time
# # ... list is [4,5,6] the second time
# end
#
# Return value: You can set the return value for the method that
# accepts the block like so:
#
# @cruncher.expects.do_things.yields(:bean1,:bean2).returns("The Results")
#
# Raising errors: You can set the raised exception for the method that
# accepts the block. NOTE: the error will be raised _after_ the block has
# been invoked.
#
# # :bean1 and :bean2 will be passed to the block, then an error is raised:
# @cruncher.expects.do_things.yields(:bean1,:bean2).raises("Too crunchy")
#
def yields(*items)
@options[:suppress_arguments_to_block] = true
if items.empty?
# Yield once
@options[:block] = lambda do |block|
if block.arity != 0 and block.arity != -1
raise ExpectationError.new("The given block was expected to have no parameter count; instead, got #{block.arity} to <#{to_s}>")
end
block.call
end
else
# Yield one or more specific items
@options[:block] = lambda do |block|
items.each do |item|
if item.kind_of?(Array)
if block.arity == item.size
# Unfold the array into the block's arguments:
block.call *item
elsif block.arity == 1
# Just pass the array in
block.call item
else
# Size mismatch
raise ExpectationError.new("Can't pass #{item.inspect} to block with arity #{block.arity} to <#{to_s}>")
end
else
if block.arity != 1
# Size mismatch
raise ExpectationError.new("Can't pass #{item.inspect} to block with arity #{block.arity} to <#{to_s}>")
end
block.call item
end
end
end
end
self
end
def to_s # :nodoc:
format_method_call_string(@options[:mock],@options[:method],@options[:arguments])
end
private
def anger(msg, mock,mname,args)
ExpectationError.new("#{msg}: expected call <#{to_s}> but was <#{format_method_call_string(mock,mname,args)}>")
end
end
end