Ruby object creation and method call debugging without mangling your gems directory


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.


Leave a Reply

%d bloggers like this: