Saturday, October 18, 2008

"Fun" with Hook Methods in Ruby

I've been wrestling with what turned out to be a rather interesting issue involving ruby hook methods. I don't yet have a solution to the problem but perhaps the activity of describing the problem in writing will illuminate a solution. Hook methods in ruby are callback methods you can implement in your code to get notified at interesting points in the lifecycle of a ruby object. The one which caused trouble in my case is inherited. By implementing the inherited method in a class you can get called back when another class inherits from it. It's quite useful when you want to do something like record each descendant of a given class, for example.

I ran into an issue with inherited while trying to understand a very strange behaviour in my IDE of choice, NetBeans. When running my rails test cases from NetBeans, I noticed they were not transactional, but when run on the command line they were. After sending a flame-o-gram which I will soon need to retract to the nb ruby mailing list, I decided to poke around in their code. I finally narrowed the problem down to their testrunner code.

In the newest version of NetBeans they have added a nicer ruby testrunner. In order to provide some features this new testrunner keeps track of all descendants of Test::Unit::TestCase. It does this, by, you guessed it, using the inherited method. When I delved into this code the first problem I saw was pretty obvious. The author had violated what I will now credit my friend Jim by referring to as Weirich's Hook Method Implementation Commandment:

Thou Shalt Always Delegate to the Previous Implementation

The reason this is important is that when you implement a hook method, you can easily step on someone else who had implemented it for another reason. Using the rails alias_method_chain will normally take care of this for you, but because this code loads before rails they didn't have that option and had not done the right thing and delegated explicitly.

And sure enough, I noticed that when I commented out the inherited method in the testrunner my tests became transactional. I figured at this point I had it licked. I went ahead and changed the NB testrunner code to delegate correctly and tried again. Sadly, it had no affect on the transactional issue. Much head scratching ensued. After many hours of investigation, I finally tracked down the real problem. I noticed that the use_transactional_fixtures class attribute of Test::Unit::TestCase was not being set. This led me to investigate how class_inheritable_attributes works in rails. Eventually I was able to put together a failing test case which expresses the problem. Here it is:

require File.dirname(__FILE__) + '/inheritable_accessor'
require File.dirname(__FILE__) + '/../test_helper'

class A
class_inheritable_accessor :foo
self.foo = "bar"
end

class B < A

end

class InheritableAttributesTest < Test::Unit::TestCase

def test_foo
assert_equal "bar", B.foo
end
end


And here what's in inheritable_accessor.rb:

class A
class << self
alias_method :a_old_inherited, :inherited

def inherited(base)
puts "A inherited"
a_old_inherited(base)
end
end
end

To try this at home, simply make a new rails project and drop these two files into your test/units directory. It should fail. But why? The inherited method does delegates to the previous implementation correctly, right? Now try switching the order of those two require statements. Poof, it passes.

Turns out the problem happens because of how class_inheritable_accessor is implemented. As you probably have guessed, it uses inherited. And yes, it does correctly delegate to the previous implementation. But it's where its implemented that is the problem: in order to let all classes be able to use this method, it's implemented on Class. However, there is an unfortunate side effect to this decision: it breaks for any class loaded before this code which itself defines inherited. In our example, the A class defines the inherited method before the rails code is loaded. But since the A class extends the Class class it's definition of inherited overrides the version rails adds to Class. The net outcome is that for the A class, and any class loaded before rails that defines inherited, class_inheritable_accessor is broken.

What's the right way to fix this? I'm not honestly sure. It could be that in order to do something like this which effectively adds a feature to the language, you need to be loaded first. On the flip side, it could be considered a bug that class_inheritable_accessor is broken in cases like this. Well, I had hoped that by the time I got this point in the post a clear solution would emerge. I suppose I'll have to leave it, as they say, as an exercise for the reader :)

Update: I was about to post this, and I went to grab some dinner and finally realized the solution: A still does not entirely obey Weirich's law. It correctly delegates to a previous implementation in the same class, but not to the superclass. A call to super at the end of the inherited method causes the test to pass. Adding the super call to the NB test runner code also causes my tests to be transactional again. Yay!