lib/spy/subroutine.rb in spy-0.2.3 vs lib/spy/subroutine.rb in spy-0.2.4
- old
+ new
@@ -4,21 +4,27 @@
# @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
- attr_reader :base_object, :method_name, :calls, :original_method, :hook_opts
+ attr_reader :base_object, :method_name, :singleton_method, :calls, :original_method, :original_method_visibility, :hook_opts
# 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
@@ -32,20 +38,22 @@
# @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]
def hook(opts = {})
- @hook_opts = opts
raise "#{base_object} method '#{method_name}' has already been hooked" if hooked?
- hook_opts[:force] ||= base_object.is_a?(Double)
+ @hook_opts = opts
+ @original_method_visibility = method_visibility_of(method_name)
hook_opts[:visibility] ||= original_method_visibility
+ hook_opts[:force] ||= base_object.is_a?(Double)
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
@@ -59,35 +67,55 @@
# @return [self]
def unhook
raise "'#{method_name}' method has not been hooked" unless hooked?
if original_method && method_owner == original_method.owner
- original_method.owner.send(:define_method, method_name, original_method)
- original_method.owner.send(original_method_visibility, method_name) if original_method_visibility
+ 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
# is the spy hooked?
# @return [Boolean]
def hooked?
- self == self.class.get(base_object, method_name, @singleton_method)
+ self == self.class.get(base_object, method_name, singleton_method)
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]
def and_return(value = nil)
- raise ArgumentError.new("value and block conflict. Choose one") if !(value.nil? || value.is_a?(Hash) && value.has_key?(:force)) && block_given?
+ @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.new("value and block conflict. Choose one")
+ end
+
@plan = Proc.new
check_for_too_many_arguments!(@plan)
else
@plan = Proc.new { value }
end
@@ -190,30 +218,29 @@
true
end
private
+ # 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`
def override_method
eval <<-METHOD, binding, __FILE__, __LINE__ + 1
__method_spy__ = self
lambda do |*__spy_args_#{self.object_id}, &block|
__method_spy__.invoke(self, __spy_args_#{self.object_id}, block, caller(1)[0])
end
METHOD
end
def clear_method!
- @hooked = false
+ @hooked = @do_not_check_plan_arity = false
@hook_opts = @original_method = @arity_range = @original_method_visibility = @method_owner= nil
end
- def original_method_visibility
- @original_method_visibility ||= method_visibility_of(method_name)
- end
-
def method_visibility_of(method_name, all = true)
- if @singleton_method
+ 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)
@@ -228,25 +255,22 @@
:private
end
end
end
- def define_method_with
- @singleton_method ? :define_singleton_method : :define_method
- end
-
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
def check_for_too_many_arguments!(block)
- return unless arity_range
+ 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}")
@@ -271,10 +295,10 @@
(min..max)
end
end
def current_method
- @singleton_method ? base_object.method(method_name) : base_object.instance_method(method_name)
+ singleton_method ? base_object.method(method_name) : base_object.instance_method(method_name)
end
def method_owner
@method_owner ||= current_method.owner
end