Reputation: 1243
I'm working on a library that will spy on method calls (SinonJS-style). What I want to do is wrap and unwrap a method. To wrap the method, I can just wrap the original method in a block:
module Spy
def on(receiver, msg)
@original = receiver.method(msg)
wrapped = Proc.new {|*args| @original}
receiver.define_singleton_method(msg, wrapped)
end
extend self
end
instance = Object.new
Spy.on(instance, :to_s)
This works fine, but unwrapping the method is problematic:
module Spy
# Add this to the above
def restore(receiver, msg)
receiver.define_singleton_method(msg, @original)
end
end
instance = Object.new
original = instance.method(:to_s)
Spy.on(instance, :to_s)
Spy.restore(instance, :to_s)
restored = instance.method(:to_s)
original == restored
=> false
original.object_id
=> 70317288647120
restored.object_id
=> 70317302643500
In fact, it looks like Object#method
always returns a new object_id. Is there a way for me to reattach the exact same method to the object? In JS I would just store the function and swap it back into place. Am I misunderstanding something about Ruby? Is there another approach that I can use? I'm really interested in the ==
comparator for testing
Thanks ahead of time!
EDIT:
A boiled down version of what the issue is:
irb(main):001:0> receiver = Object.new
=> #<Object:0x007fc4a1939320>
irb(main):002:0> original = receiver.method(:to_s)
=> #<Method: Object(Kernel)#to_s>
irb(main):003:0> original == receiver.method(:to_s)
=> true
irb(main):004:0> receiver.define_singleton_method(:to_s, original)
=> :to_s
irb(main):005:0> original == receiver.method(:to_s)
=> false
irb(main):006:0>
Is there another way to reattach a method such that the above would be true?
Upvotes: 1
Views: 1214
Reputation: 1243
It looks like Mocha accomplishes this by defining a new method which wraps the original: https://github.com/freerange/mocha/blob/master/lib/mocha/class_method.rb#L75
I'm not 100% satisfied with this solution, but it should do the job in most cases. I'm still interested if I can unattach and reattach a method to an instance
EDIT:
It turns out that I can use Method#source_location
instead for my tests. When this is spied, it will point to my Spy class, and when it is not spied, it points to the original implementation
EDIT 2:
Looked into Max's comment about where the methods were being defined and dug into Ruby's Method
object. I eventually settled on original.owner.instance_eval { define_method msg, wrapped }
to wrap and original.owner.instance_eval { define_method msg, original }
for the restore. This worked with my original tests of using Method#==
to test
Upvotes: 0
Reputation: 22325
The first problem is that the block you pass to define_singleton_method
is evaluated in the context of receiver
, so @original
will be nil
.
You can copy the method to a local variable so that the block will be a closure
@original = receiver.method(msg)
org = @original
wrapped = Proc.new { |*args| org }
Secondly, the object ID of the method is irrelevant and you should actually expect it to be different. Look at the method you actually get back:
instance.method(:to_s)
#<Method: Object(Kernel)#to_s>
Spy.on(instance, :to_s)
Spy.restore(instance, :to_s)
instance.method(:to_s)
#<Method: #<Object:0x00000001>.to_s>
The method changes from Kernel
to the singleton class! Why would the ID be the same? But this doesn't matter because all you've done is attached the method from Kernel
to the singleton class, so the same code gets run anyway.
Upvotes: 1