Blog

Dissecting Code With Ruby's caller Method

Placeholder Avatar
Raluca Pintilii
September 7, 2019

ruby location = caller.first location = location[0...location.rindex(":")] line_number = location[(location.rindex(":")+1)..-1] file = location[0...location.rindex(":")] puts location puts File.open(file).readlines[line_number.to_i - 1] puts "#{values}" puts "--"*20

This short snippet has become one of my favourite debugging tools recently. It’s not efficient, definitely not elegant, but man is it useful.

But let’s back up a moment, how did I get to this stage?

Background

As part of our consultancy service at reinteractive, we often inherit applications developed by other Rails shops of varying quality. Previous developers of the code we receive have often used, well, let’s just say “creative” approaches.

In most rails apps a line like this is pretty clear:

ruby widget.cost_of_production

Simple: Widget is an ActiveRecord model, cost_of_production is either a DB field or a method defined in Widget. Easy right? Except when widget is not an ActiveRecord class.

Off the rails

In this particular case, the previous developer(s) used immutable structs (via the Hamsterdam gem) internally. Not only so, but each ActiveRecord model would be transformed into three of these structs - raw, calculated, and combined. With combined being a wrapper that zipped data and calculated into one struct.

Basically, imagine re-implementing ActiveRecord but without any of the “magic” that makes it nice to use.

Crawling through the code was difficult to say the least. “Jump to definition” fails because the “method” you’re looking for is not a method, it is a member of a struct. Even a simple grep is misleading because the structs are combined using .merge on the hashes, so the individual “fields” may not be named in the file. Not to mention that the structs and the DB may be using different names for the same field

Debugging Ruby

Aaron Patterson (of core Ruby and core Rails fame) has an awesome blog post titled I am a puts debuggerer which should be required reading for any Rubyist, where he outlines some of the things we can do in Ruby when debugging. Perhaps the most memorable sentence is this one:

This is debugging code, so you can do whatever you want.

Debugging code is inherently throw-away code. It can be messy, inefficient, and downright ugly, as long as it’s useful.

A simple example

I’ll use OpenStruct here as a simple example of using the snippet at the top of this post, but you can drop it into any class really.

Let’s start with a basic script:

```ruby require ‘ostruct’

struct = OpenStruct.new( value: ‘complicated calc result’ )

puts “#{struct}” ```

Easy stuff. Run the code and we get:

#<OpenStruct value="complicated calc result">

Now imagine you don’t know where that value came from, and jumping through the code isn’t making things any clearer.

Luckily, Ruby allows us to re-open a class and inject our own code (aka “monkey patching”). This makes for a great debugging tool in the tool belt.

Using our own OpenStruct constructor and snippet, we get this script:

```ruby require ‘ostruct’

class OpenStruct def initialize(hash=nil) @table = {} if hash hash.each_pair do |k, v| k = k.to_sym @table[k] = v end location = caller.first location = location[0…location.rindex(“:”)] line_number = location[(location.rindex(“:”)+1)..-1] file = location[0…location.rindex(“:”)] puts location puts File.open(file).readlines[line_number.to_i - 1] puts “#{hash}” puts “–“*20 end end end

struct = OpenStruct.new( value: ‘complicated calc result’ )

puts “#{struct}” ```

The basic initialise method code here is taken straight from the OpenStruct source code, with our snippet added to the bottom of the if hash block.

So what’s happening here?

caller gives us a stack trace up to that line. For my purposes, I only care about the first entry (i.e. the point where OpenStruct.new was called).

Next, we do some quick-and-dirty parsing of that line to grab the filename and line number (the trace will be in a format like: ./<filename>:<linenum>: in '<method>')

Lastly, we print out: the file location, the line in that file that called the method I’m in, any values we want to see, and then a lot of hyphens (hyphens purely as a divider in the log as there might be a lot of these printed out).

Running the above gives:

``` ./sharp_knives.rb:25 struct = OpenStruct.new(


# ```

Location of call, actual line that does the call, and the values passed in.

Now tracing back the value allocations can be done by looking at the logs. I wouldn’t say this made the process “easy” by any means, but it was certainly an improvement over trying to trace through the convoluted logic with my brain.

Cool story bro

You might be saying to yourself “Yeah well that was cool and all for that one weird scenario, but in a normal rails app we don’t need that kind of stuff right?”

Maybe.

Since “crafting” that snippet (and yes, I’m being extremely generous with the term craft here) I’ve already used it on another project.

This time the scenario was a Rails upgrade. The code was much more conventional and, most importantly, used the money) gem.

Around version 6.7 money included a breaking change so that == would raise when comparing against other non-Money objects. i.e. Money.new(1) == 1 would now raise an exception, unless you are comparing against zero - Money.new(1) == 0 will not raise an exception.

This was pretty tricky to clear up, in part because you can’t just search your codebase for Money objects (unless you know every single Money object’s name I suppose). And in some cases the call may not be for == but rather eq (as in a spec). In one particularly troublesome case, I was actually hitting this exception inside ActiveRecord’s validations when creating a new test object (using FactoryBot), so the call stack was full of ActiveSupport and FactoryBot entries, making it difficult to pinpoint where the error was occurring in the app code. To help out, I just monkey-patched the above snippet into the Money::Arithmetic#== to have a look at what was causing the exception.

Poking around I eventually discovered that ActiveRecord’s numericality validation basically calls old_value != new_value (not an exact copy, just to give the idea) and this was causing my problem. Some more investigation and I found there was a custom validation inside the model that updates one of the fields. The problem? It was updating the raw value_cents field (i.e. not the Money-ified value version) with a Money object. This Numeric <-> Money mismatch was the root cause of my problem, and the solution was as simple as changing value_cents = <Money obj> to value = <Money obj>.

Summary

Ruby has some very powerful tools baked in (source_location is another notable mention worth knowing about), and combined with the ability to monkey-patch these into existing classes, it gives us some very useful debugging techniques that few languages can compete with.

Just remember, anything goes with debugging code. It can have terrible performance, it can be riddled with dodgy monkey-patching, it can be full of duplication. After all, there’s no point polishing it - once the bug is fixed you’ll be throwing it away.