In Progress
Unit 1, Lesson 1
In Progress

Debugging in Gems

Video transcript & code

The other day I ran into a bug in one of my programs that had me stumped. I wasn't sure of the problem, but I was certain the answers were to be found somewhere in the Rake gem source code. I needed to know exactly what was happening in that code.

I could have opened the debugger. But I have a long and fraught history with debuggers, especially in Ruby. I've had too many experiences where if I could even get the debugger to work in the first place, simply running the program under it introduced new and different errors. As a result, I tend to turn to the debugger only as a last resort. If it had been my own code, I would have simply dropped some temporary extra outputs into my code to see what was going on. But it wasn't my code.

Sooner or later we all run into a situation like this, where we need to understand what's going on in a third-party library, and it's not enough to simply read the code. We need to see what's happening at runtime. Maybe we want to test what happens when we change the third-party code.

There are ways to do this that involve downloading or unpacking the source code of the gem in question, and altering the load path or Bundler settings to use the local source code instead of the system gem. We'll talk about these techniques one day, because they are important to know. But they all take time and involve many steps. It can be tricky to get them right and be sure that our code is only interacting with the local, edited version of the gem instead of the system one.

At times like this, the quickest and dirtiest solution is to simply edit installed gems in-place. Some of you are probably gasping that I would even suggest such a thing. Think worse of me if you must; but I confess that this is a technique I sometimes use. It's the quickest and the dirtiest approach of all, but I think the quickness can sometimes justify the dirtiness.

Assuming you haven't stopped this video in disgust, I want to quickly go over how to edit gems in-place as safely as possible.

First of all, let's talk about finding where the gem source is located. Gems can be installed in a number of places: in system-wide directories, in user-specific directories, in Ruby-version-specific or gemset-specific directories managed by tools like RVM, and even in project-local vendor directories. And any of those locations may contain multiple versions of a gem.

If our project uses Bundler to manage gem dependencies, the easiest way to find the gem files which will be used by our project is to go to the command line and type bundle show, followed by the name of the gem we want to explore. Bundler then outputs the full path of the gem version in use by the project.

$ bundle show rake
/home/avdi/.rvm/gems/ruby-2.0.0-p247/gems/rake-10.1.0

Even more convenient, we can type bundle open and the name of the gem, and the gem's root directory will be opened in our default editor.

$ EDITOR="emacsclient -n"                                                        
$ bundle open rake

There are several extensions to Rubygems that make it easy to locate gem files in non-bundler-managed projects. One such project is called open-gem. After installing it, we can type gem open, followed by the name of the gem, to open up the gem in our editor.

Once we have the right files open, we can make our debugging edits and, hopefully, diagnose our problem.

def excluded_from_list?(fn)
  return true if @exclude_patterns.any? do |pat|
    case pat
    when Regexp
      fn =~ pat
    when /[*?]/
      File.fnmatch?(pat, fn, File::FNM_PATHNAME)
    else
      fn == pat
    end
  end
  @exclude_procs.any? { |p| 
    is_match = p.call(fn)
    if is_match
      warn "!!! excluding because #{p.inspect} matches #{fn}"
    end
    is_match
  }
end

OK, time has passed, we've determined the issue, and our work is done. In the process of chasing down leads we've made half a dozen different experimental edits to the gem in order to figure out what exactly was going on.

This is a time of deadly peril. We now have a gem in our system which does not match its original sources. If we get distracted now, we might forget the changes that we need to revert. This could cause no end of grief down the road if our edits changed the behavior of the gem in any way.

If this were code inside our project we'd just issue a git revert to blow away the changes. But our local Rubygem directories aren't under revision control.

Fortunately, Rubygems gives us a simple tool to revert a gem to its original state exactly as it was when first unpacked. We can type gem pristine, followed by the name of the gem, and Rubygems will use its package cache to restore the gem to its original unmodified state.

$ gem pristine rake
Restoring gems to pristine condition...
Restored rake-10.0.4
Restored rake-10.1.0

Editing gems in place is a risky way to debug, but it can be quite expedient. Be careful, remember to back out your changes, and happy hacking.

Responses