Andrey Shchekin
Andrey Shchekin

Reputation: 21599

Is it possible to run code after each line in Ruby?

I understand that it is possible to decorate methods with before and after hooks in ruby, but is it possible to do it for each line of a given method?

For example, I have an automation test and I want to verify that after each step there no error shown on the page. The error is shown as a red div and is not visible to Ruby as raise or anything like that, so I have to check for it manually (there are several other use cases as well).

I understand that it might be possible using set_trace_func. But I think this may bring more problems than benefits since it works over entire call tree (and requires me to filter it myself).

UPDATE (clarification):

I am interested in intercepting all actions (or calls) that are performed within a given method. This means unspecified number of classes is called, so I can't just intercept any given set of classes/methods.

Upvotes: 4

Views: 773

Answers (3)

Igor Kasyanchuk
Igor Kasyanchuk

Reputation: 774

What about (just tries)

    class User

      def initialize(name)
        @name = name
      end

      def say_hello
        puts "hello #{@name}"
      end

      def say_hi(friend)
        puts "hi #{@name} from #{friend}"
      end

      def say_bye(a, b = 'Anna')
        puts "bye #{a} and #{b}"
      end

    end

    User.class_eval do
      User.instance_methods(false).each do |method|
        original = instance_method(method)
        define_method method do |*options| 
          parameters = original.parameters
          if parameters.empty?
            original.bind(self).call
          else
            original.bind(self).call(*options)
          end
          puts __method__
        end
      end
    end

    user = User.new("John")

    user.say_hello
    user.say_hi("Bob")
    user.say_bye("Bob")
    user.say_bye("Bob", "Lisa")

outputs:

    hello John
    say_hello
    hi John from Bob
    say_hi
    bye Bob and Anna
    say_bye
    bye Bob and Lisa
    say_bye

Upvotes: 0

James A. Rosen
James A. Rosen

Reputation: 65232

It sounds like you want tracing on every method call on every object, but only during the span of every call to one particular method. In that case, you could just redefine the method to turn on- and off instrumentation. First, use the universal instrumentation suggsted in Pinochle's answer, then redefine the method in question as follows:

# original definition, in /lib/foo.rb:
class Foo
  def bar(baz)
    do_stuff
  end
end

# redefinition, in /test/add_instrumentation_to_foo.rb:
Foo.class_eval do
  original_bar = instance_method(:bar)
  def bar(baz)
    TracedObject.install!
    original_bar.bind(self).call(baz)
    TracedObject.uninstall!
  end
end

You'd need to write the install! and uninstall methods, but they should be pretty trivial: just set or unset a class variable and check for it in the instrumentation logic.

Upvotes: 2

Pinochle
Pinochle

Reputation: 5563

It is, and you can get a full description of how to do it in section 8.9 of The Ruby Programming Language. Running the code on each invocation of the method involves sending the method to a TracedObject class that has an implementation for method_missing. Whenever it receives a message, it invokes method_missing and executes whatever code you have assigned to it. (This is, of course, for general tracing).

That's the general description of the procedure for doing it, you can consult the book for details.

Upvotes: 2

Related Questions