n_x_l
n_x_l

Reputation: 1610

Ruby - Capture method calls sent from method body

I am looking for ways to ensure that a set of methods defined in my objects are all calling a particular method. To illustrate, say I have objects A and B, both have methods like so:

class A
  def method_a
     important_method!
  end
end

class B
  def method_b
     important_method!
  end
end

How can I easily ensure that both method_a from A and method_b are calling important_method!?

In this case important_method will come from a module that will be included in both A and B (Actually in their common superclass).

So far what I tried is to wrap both objects in a proxy that defines method_missing and collect methods calls, but this only tells me that method_a and method_b are called. Any ideas?

Upvotes: 2

Views: 322

Answers (2)

Cary Swoveland
Cary Swoveland

Reputation: 110685

My answer was initially to use TracePoint, but then I read how @user2074840 used alias, which got me thinking of using alias in conjunctions with caller, which leads to a very direct solution. I present that below as #2 Use Caller.

#1 Use TracePoint

In Ruby 2.0+ you can use TracePoint to obtain the information you need. (In earlier versions, you may be able to use Kernel#set_trace_point.)

To see how this works, let's lay down some sample code:

def a
  puts "a"
  c
  important
end

def b
  puts "b"
  important
end

def c
  puts "c"
end

def important
  puts "important"
end

We now set up for the trace, specifying the two events of interest, :call and :return, and the information we want to save, the event (:call or :return) and the method (:a, :b or :c):

events = []
trace = TracePoint.trace(:call, :return) { |tp|
  events <<  { event: tp.event, method: tp.method_id } }

Then execute the code:

4.times { send([:a, :b, :c][rand(0..2)]) }
  # b
  # important
  # b
  # important
  # a
  # c
  # important
  # c

disable trace:

trace.disable

and examine the information collected:

p events
  # [{:event=>:call,   :method=>:b},
  #  {:event=>:call,   :method=>:important},
  #  {:event=>:return, :method=>:important},
  #  {:event=>:return, :method=>:b},
  #  {:event=>:call,   :method=>:b},
  #  {:event=>:call,   :method=>:important},
  #  {:event=>:return, :method=>:important},
  #  {:event=>:return, :method=>:b},
  #  {:event=>:call,   :method=>:a},
  #  {:event=>:call,   :method=>:c},
  #  {:event=>:return, :method=>:c},
  #  {:event=>:call,   :method=>:important},
  #  {:event=>:return, :method=>:important},
  #  {:event=>:return, :method=>:a},
  #  {:event=>:call,   :method=>:c},
  #  {:event=>:return, :method=>:c}]

Note that this cannot be run in IRB or PRY.

We can now extract the calls to :important as follows:

def calls_to_method(events, method) 
  stack = []
  events.each_with_object([]) do |h, calling_methods|
    stack << h
    while stack.size > 1 &&
          stack[-1][:event] == :return &&
          stack[-2][:event] == :call &&
          stack[-1][:method] == stack[-2][:method] do
      if (stack.size > 2 && (stack[-1][:method] == method))
        calling_methods << stack[-3][:method]
      end
      stack.pop
      stack.pop
    end  
  end
end

calls = calls_to_method(events, :important)
  #=> [:b, :b, :a]

calls.uniq
  #=> [:b, :a]

#2 Use Caller

This approach uses alias and Kernel#caller:

@calling_methods = []
alias :old_important :important

def important
  @calling_methods << caller.first[/`(.*)'/,1]
  old_important
end

4.times { send([:a, :b, :c][rand(0..2)]) }
  # b
  # important
  # c
  # a
  # c
  # important
  # a
  # c
  # important
  # b
  # a
  # a

p @calling_methods
  #=> ["b", "a", "a"]

Here's an example of an array returned by caller:

caller
  #=> ["abc.rb:9:in `b'",
  #    "abc.rb:32:in `block in <main>'",
  #    "abc.rb:32:in `times'",
  #    "abc.rb:32:in `<main>'"]

It is only the first element that we use:

caller.first
  #=> "abc.rb:9:in `b'",

and we apply the regex to extract the method name, which is preceded by ''` (Ascii 96) and followed by a single quote.

Upvotes: 2

michal.samluk
michal.samluk

Reputation: 352

I've played a little with this. What do you think about such solution?

module CallImportantMethod

  def important_method!
    puts 'Important Method called!'
  end 

  def self.included(base)


    base.instance_methods(false).each do |method_name|

      base.class_eval do
        alias_method :"old_#{method_name}", method_name 
      end

      base.class_eval <<-eoruby
        def #{method_name}(*args, &block)
          important_method!
          old_#{method_name}(*args, &block)
        end
      eoruby

    end 
  end

end 

class SomeClass

  def testing 
    puts 'My Method was called'
  end 

  def testing_with_args(a,b)
    puts a + b
  end 

  def testing_with_block
    yield
  end 

  include CallImportantMethod

end 

i = SomeClass.new 
i.testing 
# => Important Method called!
# => My Method was called
i.testing_with_args(5,8)
# => Important Method called!
# => 13
i.testing_with_block { puts 'passing block...' } 
# => Important Method called!
# => passing block...

Upvotes: 1

Related Questions