class Spy::Subroutine
Constants
- SPY_ARGS_PREFIX
this returns a lambda that calls the spy object. we use eval to set the spy object id as a parameter so it can be extracted and looked up later using `Method#parameters`
Attributes
@!attribute [r] #base_object
@return [Object] the object that is being watched
@!attribute [r] #method_name
@return [Symbol] the name of the method that is being watched
@!attribute [r] #singleton_method
@return [Boolean] if the spied method is a singleton_method or not
@!attribute [r] calls
@return [Array<CallLog>] the messages that have been sent to the method
@!attribute [r] #original_method
@return [Method] the original method that was hooked if it existed
@!attribute [r] #original_method_visibility
@return [Method] the original visibility of the method that was hooked if it existed
@!attribute [r] #hook_opts
@return [Hash] the options that were sent when it was hooked
@!attribute [r] #base_object
@return [Object] the object that is being watched
@!attribute [r] #method_name
@return [Symbol] the name of the method that is being watched
@!attribute [r] #singleton_method
@return [Boolean] if the spied method is a singleton_method or not
@!attribute [r] calls
@return [Array<CallLog>] the messages that have been sent to the method
@!attribute [r] #original_method
@return [Method] the original method that was hooked if it existed
@!attribute [r] #original_method_visibility
@return [Method] the original visibility of the method that was hooked if it existed
@!attribute [r] #hook_opts
@return [Hash] the options that were sent when it was hooked
@!attribute [r] #base_object
@return [Object] the object that is being watched
@!attribute [r] #method_name
@return [Symbol] the name of the method that is being watched
@!attribute [r] #singleton_method
@return [Boolean] if the spied method is a singleton_method or not
@!attribute [r] calls
@return [Array<CallLog>] the messages that have been sent to the method
@!attribute [r] #original_method
@return [Method] the original method that was hooked if it existed
@!attribute [r] #original_method_visibility
@return [Method] the original visibility of the method that was hooked if it existed
@!attribute [r] #hook_opts
@return [Hash] the options that were sent when it was hooked
@!attribute [r] #base_object
@return [Object] the object that is being watched
@!attribute [r] #method_name
@return [Symbol] the name of the method that is being watched
@!attribute [r] #singleton_method
@return [Boolean] if the spied method is a singleton_method or not
@!attribute [r] calls
@return [Array<CallLog>] the messages that have been sent to the method
@!attribute [r] #original_method
@return [Method] the original method that was hooked if it existed
@!attribute [r] #original_method_visibility
@return [Method] the original visibility of the method that was hooked if it existed
@!attribute [r] #hook_opts
@return [Hash] the options that were sent when it was hooked
@!attribute [r] #base_object
@return [Object] the object that is being watched
@!attribute [r] #method_name
@return [Symbol] the name of the method that is being watched
@!attribute [r] #singleton_method
@return [Boolean] if the spied method is a singleton_method or not
@!attribute [r] calls
@return [Array<CallLog>] the messages that have been sent to the method
@!attribute [r] #original_method
@return [Method] the original method that was hooked if it existed
@!attribute [r] #original_method_visibility
@return [Method] the original visibility of the method that was hooked if it existed
@!attribute [r] #hook_opts
@return [Hash] the options that were sent when it was hooked
@!attribute [r] #base_object
@return [Object] the object that is being watched
@!attribute [r] #method_name
@return [Symbol] the name of the method that is being watched
@!attribute [r] #singleton_method
@return [Boolean] if the spied method is a singleton_method or not
@!attribute [r] calls
@return [Array<CallLog>] the messages that have been sent to the method
@!attribute [r] #original_method
@return [Method] the original method that was hooked if it existed
@!attribute [r] #original_method_visibility
@return [Method] the original visibility of the method that was hooked if it existed
@!attribute [r] #hook_opts
@return [Hash] the options that were sent when it was hooked
@!attribute [r] #base_object
@return [Object] the object that is being watched
@!attribute [r] #method_name
@return [Symbol] the name of the method that is being watched
@!attribute [r] #singleton_method
@return [Boolean] if the spied method is a singleton_method or not
@!attribute [r] calls
@return [Array<CallLog>] the messages that have been sent to the method
@!attribute [r] #original_method
@return [Method] the original method that was hooked if it existed
@!attribute [r] #original_method_visibility
@return [Method] the original visibility of the method that was hooked if it existed
@!attribute [r] #hook_opts
@return [Hash] the options that were sent when it was hooked
Public Class Methods
set what object and method the spy should watch @param object @param #method_name <Symbol> @param #singleton_method <Boolean> spy on the singleton method or the normal method
# File lib/spy/subroutine.rb, line 32 def initialize(object, method_name, singleton_method = true) @base_object, @method_name = object, method_name @singleton_method = singleton_method reset! end
Private Class Methods
retrieve the method spy from an object or return nil @param #base_object @param #method_name [Symbol] @param #singleton_method [Boolean] this a singleton method or a instance method? @return [Array<Subroutine>]
# File lib/spy/subroutine.rb, line 330 def get(base_object, method_name, singleton_method = true) if singleton_method if base_object.respond_to?(method_name, true) spied_method = base_object.method(method_name) end elsif (base_object.public_instance_methods + base_object.protected_instance_methods + base_object.private_instance_methods).include?(method_name) spied_method = base_object.instance_method(method_name) end if spied_method Agency.instance.find(get_spy_id(spied_method)) end end
retrieve all the spies from a given object @param #base_object @param #singleton_method [Boolean] (true) only get singleton_method_spies @return [Array<Subroutine>]
# File lib/spy/subroutine.rb, line 350 def get_spies(base_object, singleton_methods = true) all_methods = if singleton_methods base_object.public_methods(false) + base_object.protected_methods(false) + base_object.private_methods(false) else base_object.public_instance_methods(false) + base_object.protected_instance_methods(false) + base_object.private_instance_methods(false) end all_methods.map do |method_name| Agency.instance.find(get_spy_id(base_object.method(method_name))) end.compact end
@private
# File lib/spy/subroutine.rb, line 368 def get_spy_id(method) if method.parameters[0].is_a?(Array) && method.parameters[0][1] raw_id = method.parameters[0][1].to_s if raw_id.start_with?(SPY_ARGS_PREFIX) raw_id[SPY_ARGS_PREFIX.length..-1].to_i end end end
# File lib/spy/subroutine.rb, line 319 def off(base_object, method_name, singleton_method = true) spy = get(base_object, method_name, singleton_method = true) raise NoSpyError, "#{method_name} was not spied on #{base_object}" unless spy spy.unhook end
retrieve the method spy from an object or create a new one @param #base_object @param #method_name [Symbol] @param #singleton_method [Boolean] this a singleton method or a instance method? @return [Array<Subroutine>]
# File lib/spy/subroutine.rb, line 315 def on(base_object, method_name, singleton_method = true) new(base_object, method_name, singleton_method).hook end
Public Instance Methods
tells the spy to call the original method @return [self]
# File lib/spy/subroutine.rb, line 137 def and_call_through @plan = Proc.new do |*args, &block| if original_method original_method.call(*args, &block) else base_object.send(:method_missing, method_name, *args, &block) end end self end
@overload #and_raise @overload #and_raise(ExceptionClass) @overload #and_raise(ExceptionClass, message) @overload #and_raise(exception_instance)
Tells the object to raise an exception when the message is received.
@note
When you pass an exception class, the MessageExpectation will raise an instance of it, creating it with `exception` and passing `message` if specified. If the exception class initializer requires more than one parameters, you must pass in an instance and not the class, otherwise this method will raise an ArgumentError exception.
@return [self]
# File lib/spy/subroutine.rb, line 164 def and_raise(exception = RuntimeError, message = nil) if exception.respond_to?(:exception) exception = message ? exception.exception(message) : exception.exception end @plan = Proc.new { raise exception } self end
@overload #and_return(value) @overload #and_return(&block)
Tells the spy to return a value when the method is called.
If a block is sent it will execute the block when the method is called. The airty of the block will be checked against the original method when you first call `and_return` and when the method is called.
If you want to disable the arity checking just pass `{force: true}` to the value
@example
spy.and_return(true) spy.and_return { true } spy.and_return(force: true) { |invalid_arity| true }
@return [self]
# File lib/spy/subroutine.rb, line 107 def and_return(value = nil) @do_not_check_plan_arity = false if block_given? if value.is_a?(Hash) && value.has_key?(:force) @do_not_check_plan_arity = !!value[:force] elsif !value.nil? raise ArgumentError, "value and block conflict. Choose one" end @plan = Proc.new check_for_too_many_arguments!(@plan) else @plan = Proc.new { value } end self end
@overload #and_throw(symbol) @overload #and_throw(symbol, object)
Tells the object to throw a symbol (with the object if that form is used) when the message is received.
@return [self]
# File lib/spy/subroutine.rb, line 180 def and_throw(*args) @plan = Proc.new { throw(*args) } self end
Tells the object to yield one or more args to a block when the message is received. @return [self]
# File lib/spy/subroutine.rb, line 127 def and_yield(*args) yield eval_context = Object.new if block_given? @plan = Proc.new do |&block| eval_context.instance_exec(*args, &block) end self end
if the method was called it will return true @return [Boolean]
# File lib/spy/subroutine.rb, line 187 def has_been_called? raise NeverHookedError unless @was_hooked calls.size > 0 end
check if the method was called with the exact arguments @param args Arguments that should have been sent to the method @return [Boolean]
# File lib/spy/subroutine.rb, line 195 def has_been_called_with?(*args) raise NeverHookedError unless @was_hooked match = block_given? ? Proc.new : proc { |call| call.args == args } calls.any?(&match) end
hooks the method into the object and stashes original method if it exists @param [Hash] opts what do do when hooking into a method @option opts [Boolean] force (false) if set to true will hook the method even if it doesn't exist @option opts [Symbol<:public, :protected, :private>] visibility overrides visibility with whatever method is given @return [self]
# File lib/spy/subroutine.rb, line 43 def hook(opts = {}) raise AlreadyHookedError, "#{base_object} method '#{method_name}' has already been hooked" if self.class.get(base_object, method_name, singleton_method) @hook_opts = opts @original_method_visibility = method_visibility_of(method_name) hook_opts[:visibility] ||= original_method_visibility if original_method_visibility || !hook_opts[:force] @original_method = current_method end define_method_with = singleton_method ? :define_singleton_method : :define_method base_object.send(define_method_with, method_name, override_method) if [:public, :protected, :private].include? hook_opts[:visibility] method_owner.send(hook_opts[:visibility], method_name) end Agency.instance.recruit(self) @was_hooked = true self end
is the spy hooked? @return [Boolean]
# File lib/spy/subroutine.rb, line 85 def hooked? self == self.class.get(base_object, method_name, singleton_method) end
invoke that the method has been called. You really shouldn't use this method.
# File lib/spy/subroutine.rb, line 203 def invoke(object, args, block, called_from) check_arity!(args.size) result = if @plan check_for_too_many_arguments!(@plan) @plan.call(*args, &block) end ensure calls << CallLog.new(object, called_from, args, block, result) end
reset the call log
# File lib/spy/subroutine.rb, line 214 def reset! @was_hooked = false @calls = [] clear_method! true end
unhooks method from object @return [self]
# File lib/spy/subroutine.rb, line 68 def unhook raise NeverHookedError, "'#{method_name}' method has not been hooked" unless hooked? if original_method && method_owner == original_method.owner method_owner.send(:define_method, method_name, original_method) method_owner.send(original_method_visibility, method_name) if original_method_visibility else method_owner.send(:remove_method, method_name) end clear_method! Agency.instance.retire(self) self end
Private Instance Methods
# File lib/spy/subroutine.rb, line 281 def arity_range @arity_range ||= if original_method min = max = 0 original_method.parameters.each do |type,_| case type when :req min += 1 max += 1 when :opt max += 1 when :rest max = Float::INFINITY end end (min..max) end end
# File lib/spy/subroutine.rb, line 261 def check_arity!(arity) return unless arity_range if arity < arity_range.min raise ArgumentError.new("wrong number of arguments (#{arity} for #{arity_range.min})") elsif arity > arity_range.max raise ArgumentError.new("wrong number of arguments (#{arity} for #{arity_range.max})") end true end
# File lib/spy/subroutine.rb, line 271 def check_for_too_many_arguments!(block) return if @do_not_check_plan_arity || arity_range.nil? min_arity = block.arity min_arity = min_arity.abs - 1 if min_arity < 0 if min_arity > arity_range.max raise ArgumentError.new("block requires #{min_arity} arguments while original_method require a maximum of #{arity_range.max}") end end
# File lib/spy/subroutine.rb, line 236 def clear_method! @hooked = @do_not_check_plan_arity = false @hook_opts = @original_method = @arity_range = @original_method_visibility = @method_owner= nil end
# File lib/spy/subroutine.rb, line 300 def current_method singleton_method ? base_object.method(method_name) : base_object.instance_method(method_name) end
# File lib/spy/subroutine.rb, line 304 def method_owner @method_owner ||= current_method.owner end
# File lib/spy/subroutine.rb, line 241 def method_visibility_of(method_name, all = true) if singleton_method if base_object.public_methods(all).include?(method_name) :public elsif base_object.protected_methods(all).include?(method_name) :protected elsif base_object.private_methods(all).include?(method_name) :private end else if base_object.public_instance_methods(all).include?(method_name) :public elsif base_object.protected_instance_methods(all).include?(method_name) :protected elsif base_object.private_instance_methods(all).include?(method_name) :private end end end
# File lib/spy/subroutine.rb, line 227 def override_method eval <<-METHOD, binding, __FILE__, __LINE__ + 1 __method_spy__ = self lambda do |*#{SPY_ARGS_PREFIX}#{self.object_id}, &block| __method_spy__.invoke(self, #{SPY_ARGS_PREFIX}#{self.object_id}, block, caller(1)[0]) end METHOD end