lib/spy/subroutine.rb in spy-1.0.1 vs lib/spy/subroutine.rb in spy-1.0.2

- old
+ new

@@ -31,10 +31,11 @@ # @param singleton_method <Boolean> spy on the singleton method or the normal method def initialize(object, method_name, singleton_method = true) @base_object, @method_name = object, method_name @singleton_method = singleton_method @plan = nil + @call_through = false reset! 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 @@ -56,18 +57,18 @@ original_method.owner.send(:remove_method, method_name) end if singleton_method if base_object.singleton_class.method_defined?(method_name) || base_object.singleton_class.private_method_defined?(method_name) - base_object.singleton_class.alias_method(method_name, method_name) + base_object.singleton_class.send(:alias_method, method_name, method_name) end base_object.define_singleton_method(method_name, override_method) else if base_object.method_defined?(method_name) || base_object.private_method_defined?(method_name) - base_object.alias_method(method_name, method_name) + base_object.send(:alias_method, method_name, method_name) end - base_object.define_method(method_name, override_method) + base_object.send(:define_method, method_name, override_method) end if [:public, :protected, :private].include? hook_opts[:visibility] method_owner.send(hook_opts[:visibility], method_name) end @@ -146,32 +147,11 @@ end # tells the spy to call the original method # @return [self] def and_call_through - if @base_object.is_a? Class - @plan = Proc.new do |object, *args, &block| - if original_method - if original_method.is_a? UnboundMethod - bound_method = original_method.bind(object) - bound_method.call(*args, &block) - else - original_method.call(*args, &block) - end - else - base_object.send(:method_missing, method_name, *args, &block) - end - end - else - @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 - end + @call_through = true self end # @overload and_raise @@ -230,21 +210,21 @@ # invoke that the method has been called. You really shouldn't use this # method. def invoke(object, args, block, called_from) check_arity!(args.size) - if base_object.is_a? Class - result = if @plan - check_for_too_many_arguments!(@plan) - @plan.call(object, *args, &block) - end - else - result = if @plan - check_for_too_many_arguments!(@plan) - @plan.call(*args, &block) - end - end + result = + if @call_through + call_plan(build_call_through_plan(object), block, *args) + elsif @plan + check_for_too_many_arguments!(@plan) + if base_object.is_a? Class + call_plan(@plan, block, object, *args) + else + call_plan(@plan, block, *args) + end + end ensure calls << CallLog.new(object, called_from, args, block, result) end # reset the call log @@ -351,9 +331,42 @@ singleton_method ? base_object.method(method_name) : base_object.instance_method(method_name) end def method_owner @method_owner ||= current_method.owner + end + + def build_call_through_plan(object) + if original_method + if @base_object.is_a?(Class) && original_method.is_a?(UnboundMethod) + original_method.bind(object) + else + original_method + end + else + Proc.new do |*args, &block| + base_object.send(:method_missing, method_name, *args, &block) + end + end + end + + def call_plan(plan, block, *args) + if ruby_27_last_arg_hash?(args) + *prefix, last = args + plan.call(*prefix, **last, &block) + else + plan.call(*args, &block) + end + end + + # Ruby 2.7 gives a deprecation warning about passing hash as last argument for a method + # with a double-splat operator (**), and Ruby 3 raises an ArgumentError exception. + # This checks if args has a hash as last element to extract it and pass it with double-splat to avoid an exception. + def ruby_27_last_arg_hash?(args) + last = args.last + last.instance_of?(Hash) && + !last.empty? && + Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.7.0") end class << self # retrieve the method spy from an object or create a new one