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