Ruby meta-hell

So, there's a certain type of test that's been failing in the M7 code, and I've been working on it all day long. It has to do with the Erector 0.5.1 gem from Pivotal Labs. Now, they've since moved on in versions (all the way up to 0.7.3), but for various reasons, I'd like to avoid upgrading the gem right now. (I'm pretty sure I remember hearing there was an issue with upgrading Erector sometime in Week #1, and regardless, it was an upgrade that got us into this test fixing party in the first place – I don't want to add any complexity by upgrading more stuff that will certainly break existing code before I've dealt with these changes). Plus, I don't even know for sure if the upgrade would fix the problem we're having with these particular tests.

I think this is a good time to pause and let you all snicker a bit (yes, dad, you too), catch your breath, compose yourselves, and... can we move on now? Are you sure? (Thank you, WordPress, for giving me the power to moderate comments!)

Well then, moving on.

So, Erector hooks into Rails' ActionController rendering via the following code:

ActionController::Base.class_eval do
  def render_widget(widget_class, assigns=nil)
    @__widget_class = widget_class
    if assigns
      @__widget_assigns = assigns
    else
      @__widget_assigns = {}
      variables = instance_variable_names
      variables -= protected_instance_variables
      variables.each do |name|
        @__widget_assigns[name.sub('@', "")] = instance_variable_get(name)
      end
    end
    response.template.send(:_evaluate_assigns_and_ivars)
    render :inline => ""
  end


  def render_with_erector_widget(*options, &block)
    if options.first.is_a?(Hash) && widget = options.first.delete(:widget)
      render_widget widget, @assigns, &block
    else
      render_without_erector_widget *options, &block
    end
  end
  alias_method_chain :render, :erector_widget
end

This is all well and good, except that we also test views individually via RSpec, which means we have to fudge variables (on any Erector widgets we test that take variables) that would normally get set through the controller. When you're just rendering a template, Rails gives you a mechanism to set local variables at this point, via:

render 'some/template', :locals => { :some_local_var => 'some value' }

You'll notice up above that, instead of passing the options along when rendering the widget (in the case where :widget is given as the first option), the substituted rendering functionality passes the value of @assigns. This works when you're using the widget through a controller, but breaks down when you try to render the widget on its own.

So, when we call the following line in one of our tests:

render :widget => Views::ScriptVersions::Show, :script_version => @script

The variable script_version that's local to the widget does not get set to the value of @script, like we'd want it to. And trying various combinations of :locals => {} didn't work either (as it would on a regular template render. I was consoled by the fact that at least one other person seemed to be dealing with a similar problem in this area of the code. However, I figured I was unlikely to get a fix for it (and for that version of the gem), as the later versions seemed to address the need to set variables without using the controller (though I still wasn't sure how easy this new way of doing things would be to put into an RSpec test). Not an option unless I wanted to disrupt a bunch of code with the newer syntax, etc.

So, after a bit of digging through the rspec gem code, I realized that the Spec::Rails::Example::ViewExampleGroup class, which the various rspec view tests inherit from, did contain a reference to a controller in order to do its magic without us needing to explicitly set one up. And that meant I could just set the @assigns local variable on that via some simple Ruby reflection magic (thank you, Ruby!) before rendering the widget, and the widget would pick up the variable as if it had been set in the controller:

controller.instance_variable_set(:@assigns, { :script_version => @script })
render :widget => Views::ScriptVersions::Show

And voila! All tests pass!

Ruby is really a great language to work in. It lets you do so many things that would take you much longer (and look much uglier) in another language. The price for this is that when something goes wrong, you have to decipher all the nifty meta-hacking, which is usually a great deal more time consuming than in other languages. Trade offs. You can never quite get rid of them. I sometimes wonder if the time one saves by coding in Ruby is completely negated by the amount of time he or she spends on treasure hunts like this. That said, the beauty of open source is that I was actually able to step through this code, line by line in the debugger (my new favourite thing to do!), to see what was happening, which is something you can't always do. I've run into similarly arcane problems while programming in Cocoa without that luxury.