The Problem: Debugging Calls to Code in Gems
Say you’re having a problem that ultimately doesn’t manifest itself until you get somewhere in gem source code, and the exception or symptoms don’t clearly indicate why things are breaking. This can especially be the case with test setups that leak mocks or state between tests. You can, of course edit the gem source, but why not alias the original method and debug with a substitute method that calls the original?
Hooking into new via an alias
If you have some hunches about object initialization going awry between working and failing versions of a test, you can declare the following troubleshoot_new
method (wherever, as long as it’s accessible in your example):
def troubleshoot_new(klass)
klass.class_eval do
class << self
alias :old_new :new
def new(*args)
puts self.to_s + args.inspect
old_new *args
end
end
end
end
The above code aliases the original new
method on the class and inserts a logging of the class name and arguments sent to new
before calling the original. This could be replaced by a hook to binding.pry
or byebug
to drop into a debugger instead. Then you could call troubleshoot_new(ClassName)
as follows in your individual examples to track object creations.
class Box # arbitrary class
def initialize(x, y, z)
end
end
troubleshoot_new(Box)
Then on object instantiation you’ll get the logging (the Box[1,2,3]
)
irb(main):027:0> Box.new(1,2,3)
Box[1, 2, 3]
=> #<Box:0x00000001047c7d38>
Cleaning up
If you want to clean up after the alias
def untroubleshoot_new(klass)
klass.class_eval do
class << self
alias :new :old_new
end
end
end
untroubleshoot_new(Box)
Which will alias the old method back:
irb(main):029:0> Box.new(1,2,3)
=> #<Box:0x00000001047576c8>
irb(main):030:0>
Using a block to wrap the alias/unalias
To be a little more assured that the alias/unalias will happen, this could be implemented in block form instead:
def troubleshoot_new(klass)
klass.class_eval do
class << self
alias :old_new :new
def new(*args)
puts self.to_s + args.inspect
old_new *args
end
end
end
yield
klass.class_eval do
class << self
alias :new :old_new
end
end
end
irb(main):030:1* troubleshoot_new(Box) do
irb(main):031:1* p Box.new(1,2,3)
irb(main):032:0> end
irb(main):033:0> p Box.new(4,5,6)
Box[1, 2, 3]
#<Box:0x0000000104886440>
#<Box:0x00000001048860f8>
Active debugging via a hook into the object creation
Another way you can use this is to drop directly into a method deep in the call chain instead of having to debug you way into it with step
in pry
. I extended the new strategy to an arbitrary method (instance method shown here), since it’s often a specific method call that’s the trigger for a failure.
def troubleshoot(klass, method)
klass.class_eval do
alias_method "old_#{method}".to_sym, method
define_method method do |*args|
puts klass.to_s + ':' + method.to_s + "->" + args.inspect
# binding.pry # to debug from here
send "old_#{method}".to_sym, *args
end
end
yield
klass.class_eval do
alias_method method, "old_#{method}".to_sym
end
end
class Box
attr_accessor :x, :y, :z
def initialize(x, y, z)
@x=x
@y=y
@z=z
end
def join_it(insert)
puts [x,y,z].join(insert).inspect
end
end
troubleshoot(Box, :join_it) do
box = Box.new(1,2,3)
box.join_it("<->")
end
#> Box:join_it->["<->"]
#> "1<->2<->3"
The commented out binding.pry
can be uncommented to jump directly into the entry point for a problem.
May your problems rarely be this deep
Having to dig into problems that are deep in gem source means that you’re probably already in a world of pain, but hopefully the above strategy will inspire you with additional debugging tools if the worst case debugging scenario visits you.