How I debug with Ruby

Over the many years that I have been working with Ruby, I have never really learned to use a debugger, and really only lean on tools like Pry to start a REPL session at a particular line in some Ruby code. From there, I reach for Ruby's built in debugging tools, and thus far, they have served me well! Below is a non-exhaustive list of some of the common techniques I use to work out what the heck is going on with some piece of code.

How do I work out what methods are available on a class?

The methods method lets you see which methods an class defines. This is useful when working with an un-documented or otherwise unknown class, but have an idea of what you might want to achieve. The return value of this call is an array of symbols that represent method names.

Foo.methods #=> [:bar, :baz, ...]

If you run this yourself on a class, you'll see a huge list of method names, including things like :new, :==, :inspect. This is is because method also returns a list of methods that the ancestors of your object respond to. If you want to clean up this you can:

Pass the false argument in to methods:

Foo.methods(false) => [:bar, :baz]

Or selectively remove the methods of known ancestors from the resulting output:

Foo.methods - Object.methods => [:bar, :baz]

There is also the instance_methods method that does the same thing, but only returns instance methods.

The following calls are almost the same:

Foo.new.methods #=> includes methods that belong to ancestors

Foo.instance_methods(false)

Ok cool, now we have an idea of the methods, but what about actually calling them?

The parameters method lets you take a peak at the arguments required to call a method. This is not always bulletproof, as some methods take a variable number of args (e.g. def foo(*args))

Anyway, once we have a method to inspect, we can use parameters like so:

class Foo
    def bar(one:, two:)
    end

    def baz(one, two = nil, three: nil)
    end
end

Foo.instance_method(:bar).parameters #=> [[:keyreq, :one], [:keyreq, :two]]

Foo.instance_method(:baz) #=> [[:req, :one], [:opt, :two], [:key, :three]]

You can even see if a method has a default provided (:key, :opt), or is required (:req, :keyreq) along with its name.

Workout where a method is defined.

Sometimes, you're calling a method and the behaviour being presented is not at all what you expect, or even worse, your myriad of puts and raise calls you've put in place to debug the behaviour are not being called!

Here's an example:

# source/foo.rb

class Foo
    def bar(arg)
        raise "here".inspect
        # do the method here
    end
end

Foo.new.bar(arg) #=> Nothing is raised 😱

Inspecting the bar method with source_location should help work out what is going on here:

Foo.instance_method(:bar).source_location #=> ['some_monkey_patch.rb']

In our case here, we can see that something else is being called instead of our method in our class! While this exact example is not super common, I have been bitten by this a couple of times by some gems implementing a to_h, to_json, to_s etc. method on basic Ruby classes that then cause unexpected behaviour.

Another variation of this is when you have a super-class that is calling super in its initialiser (or some other place) but you don't know where that goes.

class Foo < Bar
    def call
        puts method(:call).super_method.source_location #=> ['/gems/path/bar.rb#42']
        # super
    end
end

You can then use something like cat in the terminal to inspect the file and get a sense of what Bar's super method will call.

Or better than cat, you can use bundle open to dig around a gem with your editor of choice (discussed in the next section).

Inspecting a Gem

Sometimes, the code you're having trouble with is in a gem! There are a couple of ways to go about inspecting this code. If the code is hosted on Github, then that's a fairly simple way of spelunking through code, with the benefit of having commit messages right there that could explain the behaviour you're seeing (just make sure you're looking at the right version).

But, you can also use bundle open gemname to have bundler open the correct version of the gem in your EDITOR of choice. From here, you can throw in debugging statements, make changes, and even fix a bug or two if you find any :)

If you're spelunking has lead you to drop puts statements through many files, you can clean the gem up quickly with gem pristine gemname (or gem pristine --all if your debugging journey spanned multiple gems!).

How did we get here?

We've discussed working out where a method call will take us, but what about how we got to a particular method.

For this, we can use caller. caller will print out a massive stack trace showing you how we got here, but the first line is the one that really matters:

class Foo
    def bar
        caller
    end
end

irb(main):072:0> Foo.new.bar #=> ["(irb):72:in `<top (required)>'","/Users/danielnitsikopoulos/.asdf/installs/ruby/3.2.0/lib/ruby/3.2.0/irb/workspace.rb:119:in `eval'", ...]

in this case, I'm running the code in irb, so (irb):72 is where this is being called from.

Formatting

The last thing I want to touch on, is how to do some basic formatting to make inspecting output a little easier.

Add some markers to split puts calls, especially when calling something in a loop:

def foo
    puts "Doing something:"
    do_something
    puts "*" * 88 # print out 88 stars
end

2.times { foo } #=>

Doing something:
****************************************************************************************
Doing something:
****************************************************************************************

When looking through a collection of data to understand something about it, also print out things like IDs so that you can pick out the one you want:

some_collection.map {|c| [c.id, c.name] } #=>
[
  [123, "foo"],
  [234, "bar"],
  [345, "baz"],
  ...
]

Conclusion

Ruby's built in meta programming tools are super powerful, and while there are definitely a bunch more techniques for debugging, these are the ones I always turn to first and usually are all I need to work out what is happening. I will (one day) get RubyMine's built in debugger working and report back if it blows any of this out of the water :D

Published

by Daniel Nitsikopoulos

in posts

Tagged

Β© 2010 - 2024 Daniel Nitsikopoulos. All rights reserved.

πŸ•ΈπŸ’  →