Thursday, May 25, 2006

Assignment in Ruby - Block Assignment

For the second installment of Assignment in Ruby, I would like to explore block assignment. Block assignment is very similar to the simple scoped assignments I explored in my first post. To understand the differences, it is necessary to understand how block variables are scoped.

When a block uses a local variable, Ruby has to determine what is being used. { |x, y, z| puts x, y, z } uses three local variables. Interestingly, these three variables could each potentially have a different scope.

If there is an x in the current local scope, the variable x used in the block will be that local variable, meaning the assignment will be an :lasgn (local assignment).

If there is no y in the current local scope but there is a y in an enclosing block, the y variable used will be the block scoped variable from the outer block and the assignment will be a :dasgn (block assignment).

If there is no z in the current local scope and there is no z in any enclosing blocks, then a new z will be created in the scope of the current block and the assignment will be a :dasgn_curr (block local assignment).

The different scopes are each illustrated in the following code:
$ echo "x = 123; foo() {|x| y = 456; bar() {|x,y,z| puts x, y, z}}" | parse_tree_show -f
[[:lasgn, :x, [:lit, 123]],
[:iter,
[:fcall, :foo],
[:lasgn, :x],
[:block,
[:dasgn_curr, :y],
[:dasgn_curr, :y, [:lit, 456]],
[:iter,
[:fcall, :bar],
[:masgn, [:array, [:lasgn, :x], [:dasgn, :y], [:dasgn_curr, :z]]],
[:fcall, :puts, [:array, [:lvar, :x], [:dvar, :y], [:dvar, :z]]]]]]]

Written a little more nicely, the code under inspection is:
x = 123
foo() { |x|
y = 456
bar() { |x,y,z|
puts x, y, z
}
}

Note the 2nd to last line of the S expression [:masgn, [:array, [:lasgn, :x], [:dasgn, :y], [:dasgn_curr, :z]]]. The first thing to notice is:masgn. :masgn stands for multiple assignment and will be covered in its own post. After the :masgn you can see concrete examples of the local assignment to x, the outer block assignment to y and the current block assignment to z.

This can be a sticky situation and I know I missed it when I was learning Ruby. I only internalized how block scopes are handled when I saw it reflected in ParseTree and the AST. I still don't really understand why you would want to reuse local scoped variables inside a block but that is probably an indication of my weakness with closures. If you have any ideas, please let me know.

I hope you have enjoyed another brief forray into Assignment in Ruby. Join me next time as we explore multiple assignment and how it can simplify your life.

Climbing without a Net

Developing test first is like rock climbing on lead.

When a climber climbs lead, they have to place protection as they go. At any point, if they fall, their last piece of protection generally keeps them from falling far. Sometimes, that protection will fail and when it does, the other protection they have placed keeps them from death or serious injury.

Each test written is like a piece of protection we have placed. Usually, the last test we wrote is the most important but all the tests together keep us from serious injury. Our tests really are our safety net and I can't understand why people choose to live without it.

If test first is lead climbing, then it goes to reason that the test first challenge is top roping and developing without tests is free climbing. Sure the best climbers can skate up easy climbs without protection but for the rest of us, that is the path to certain doom.

Monday, May 22, 2006

When Rails Needs a Clue - Single Table Inheritance Problems

Given Grandchild < Child < Parent < ActiveRecord::Base, your controllers need model :grandchild. Without it, well, this could happen:

There is a sinking feeling in your gut. You've been working in rails for weeks. You've been doing everything right, Test Driven Development to the hilt, when suddenly, the worst happens. Behavior in test doesn't match behavior in development. You begin to question whether any of your testing is valid. Does your app really work, or does it just seem to work in test? How can test and development possibly differ? Your worst nightmares are coming true.

This might seem like a ridiculous scenario but it is exactly what I have dealt with for the last week. We have a table with single table inheritance. Admin < Broker < User < ActiveRecord::Base all in table :users. Errors started showing up in development that passed in test. We tracked it down to differing sql statements. In test, Broker.find_all generated SELECT * FROM users WHERE ( (users.`type` = 'Broker' OR users.`type` = 'Admin' ) ). In development, the exact same Broker.find_all generated SELECT * FROM users WHERE ( (users.`type` = 'Broker' ) ).

We had no idea what was going on and we had features that had to be ready right now for a demo. We worked around the problem. We wrote our own find conditions and moved on, writing a todo to find out what was wrong. The sleepless nights started and the worry began to grow.

Over the weekend, the other developer on my team looked at some code I wrote Friday and had problems. He didn't have time to look into it but told me about it this morning. My first response, well, I'll write a test that shows the problem, only every test I wrote worked fine in test. I could reproduce his problems in development every time. This shouldn't be so hard.

Start from the basics. I created a test with exactly the same input as was reported in the error on development. Same parameters. Same session. Same flash. Everything was exactly the same. Only, what failed in development passed in test. The sick feeling in my stomach is getting stronger. I feel like I'm in a dream watching my career go down the drain. Sure enough, I track it down to nearly the same problem as before. We have Default < ActiveRecord::Base; belongs_to Broker. If the default.broker_id pointed to a user with type=Admin, it failed in development. Default.broker was returning nil when it should have been returning an Admin. Again, the SQL was different between test and development in exactly the same way. Development would only accept Brokers and test accepted Brokers and Admins. This didn't make any sense and this time, we had time to look at it.

My partner started looking at the source in Rails and I started searching online. I didn't find anything with a few quick searches. I knew the #rubyonrails channel on Freenode is pretty active so I decided to ask my question there. "Has anyone seen differences between test and development when..." Hmm. I want the exact SQL I'm seeing in test and development before I ask this question. I know, I'll fire up the console for both and get it from the logs that way.

Loki took pity on me here and Coyote found something else to toy with. export RAILS_ENV=test; ./script/console. So far, so good. Broker.find_all; I only get Brokers back. WTF! That's the same thing I've been seeing in development. A quick check. Yes, I'm in test. Hmm. User.find_all; I get everything. (select * from users, no where clause) Admin.find_all; I get only Admins. I expected that, Admin doesn't have any children. Broker.find_all; Holy hand grenades. That is everything, Admins included. The where clause changed. Umm. But. Maybe this time I put something different. Just to be sure, I used up arrow to run the first Broker.find_all; (I couldn't see a difference but who knows at this point.) Sure enough, the original search turns up the new results. Then, wham, lightning struck. If Admin has never been loaded, Rails doesn't know Admin < Broker!

A quick conversation with my partner, a little test (added model :admin to the controller) and sure enough, everything is peachy. There is just one nagging doubt. I don't know how to create this in test. You see, single table inheritance is implemented in a single YAML fixture. When fixtures :users runs, Rails figures out I have a User, Broker, Customer (< User) and Admin model. Loading my fixture seems to make Rails aware of those relationships. So, how do I get data for admins into the table for users without making Rails aware of the Admin class? I have no idea.

The sinking feeling isn't gone but it is a good deal smaller. I now have a picture of the world that explains the unexplainable and test and development work the same again. But still, that little doubt.

If you know how to test this, won't you please let me in on the secret?

Friday, May 19, 2006

Lost but not Forgotten

I still owe you several posts on the 12 kinds of Ruby assignment. I also have more to write about my sagas with ParseTree and the like. I can't promise to have them soon but I do promise to have them, eventually. In the meantime, consider this my formal apology for letting you down.

flunk, my autotest friend

You use autotest, right? (gem install ZenTest and get on it if you don't). Well, then you know autotest auto-focuses on failing tests. You probably also love this and rely on it all the time. Well, leverage flunk and take your relationship with autotest to the next level.
test_some_silly_thing
blah, blah, blah, blah
this is a test that should be much smaller but isn't
or is mystifying me with its strange behavior or something
and I want to debug what the F@#! is going on, and
I don't want to run the other 4800 tests that take more
than the 8 milliseconds attention span I am willing to
wait for my tests to run or, ... you get the idea
end

Now, lets make autotest focus on test_some_silly_thing. Add the following line as the last line of the test:

flunk "Passed!"

Cool! Now, even if our test passes, we fail. This means autotest will stay focused while we play. You can now do things like check the database after your test runs (because it is the only test that was run and no other tests had a chance to muck things up).

By the way, if you don't already know, flunk is the equivalent of assert false. It is so much easier to read and understand than assert false is though.

A few links you might find useful:
ZenTest
Using autotest with Rails
article on ZenTest in Linux Journal
Dr. Brain rails against his own creation
Everything else Dr. Brain said about ZenTest in his blog