Tagged: monkey patch Toggle Comment Threads | Keyboard Shortcuts

  • ThomasPowell 11:07 pm on June 14, 2022 Permalink | Reply
    Tags: alias, define_method, metaprogramming, monkey patch   

    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.

     
  • tech0x20 3:29 pm on November 22, 2013 Permalink | Reply
    Tags: delayed_job, monkey patch   

    Monkey patching if you need to force an error in delayed_job 

    I wanted to force an error condition in my cucumber tests, but the code ran through a DelayedJob, so I couldn’t redefine the code running under the .delay chain, because the DelayedJob worker will reload the normal code base for its processing. I finally realized that simply patching the delay instance method to return self for the class in question would bypass the delayed job.

    def force_query_error
      ObjectDelayed.class_eval do
        alias :old_delay :delay
        alias :old_query :query
    
        def delay
          self
        end
    
        def query
          raise 'monkey'
        end
      end
    end
    
    def restore_query
      ObjectDelayed.class_eval do
        remove_method :query
        remove_method :delay
        alias :query :old_query
        alias :delay :old_delay
      end
    end
    

    This is not to be confused will actually allowing DelayedJob to do its thing:

    When(/^I wait for processing of jobs$/) do
      Delayed::Worker.new(:quiet => false).work_off
      Timeout::timeout(10) do
        until Delayed::Job.count == 0 do
        end
      end
    end
    
     
c
Compose new post
j
Next post/Next comment
k
Previous post/Previous comment
r
Reply
e
Edit
o
Show/Hide comments
t
Go to top
l
Go to login
h
Show/Hide help
shift + esc
Cancel
%d bloggers like this: