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 @plan = nil 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 342 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 362 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 380 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 331 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 327 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 138 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 165 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 108 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
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 128 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 188 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 196 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 44 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 86 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 204 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 215 def reset! @was_hooked = false @calls = [] clear_method! true end
unhooks method from object @return [self]
# File lib/spy/subroutine.rb, line 69 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 282 def arity_range @arity_range ||= if original_method min = max = 0 key_args = false opt_keys = false original_method.parameters.each do |type,_| case type when :req min += 1 max += 1 when :opt max += 1 when :rest max = Float::INFINITY when :keyreq key_args = true when :keyrest, :key key_args = true opt_keys = true end end if key_args max += 1 min += 1 unless opt_keys end (min..max) end end
# File lib/spy/subroutine.rb, line 262 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 272 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 237 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 312 def current_method singleton_method ? base_object.method(method_name) : base_object.instance_method(method_name) end
# File lib/spy/subroutine.rb, line 316 def method_owner @method_owner ||= current_method.owner end
# File lib/spy/subroutine.rb, line 242 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 228 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