Jake Scruggs

writes ruby/wears crazy shirts

Every time I create a new rails project I usually put off writing tasks to analyze the code's quality 'cause it takes time and time is, you know, finite. So I've decided to extract some code into a rails plugin which I call metric_fu.

It's a bunch of rake tasks that produce reports on code coverage (using Rcov), cyclomatic complexity (using Saikuro), flog scores (using Flog), and rails stats (using 'rake stats'). It knows if it's being run inside a CruiseControl.rb build and puts the output in the Custom Build Artifacts folder so when you view a build you see this:

Cruise control build page

The coverage report is your standard rcov report:
Rcov output

Flog output is thrown into an html file:
Flog output

At the end metric_fu calculates the average flog score per method:
Flog average score
You might want to check out my previous posts on what to do with a Flog report: The Method Hit List and When You Should Ignore Metrics

Saikuro's output is the same as always:
Cyclomatic complexity output
(I changed the warning and error levels for this pic -- more on how I did that later)

And 'rake stats' is always useful:
Stats output

So how do you get all these reports?
1. install Flog
sudo gem install flog

2. install rcov
sudo gem install rcov

3. install metric_fu
ruby script/plugin install \
(in the base of your rails app)

4. rake metrics:all

Which should work fine if you have standard Rails testing and you like my defaults. But what if you use a combination of RSpec and stock Rails testing? Then you can insert this into your Rakefile:

namespace :metrics do
TEST_PATHS_FOR_RCOV = ['spec/**/*_spec.rb', 'test/**/*_test.rb']
The namespace isn't strictly necessary, but I like it for intentional purposes. Multiple paths are useful if, like on my last project, you need to be specific about which tests to run as some tests go after external services (and the people who manage them get cranky if you hammer 'em a lot).

If you also want Rcov to sort by lines of code (loc) and have more aggressive cyclomatic complexity settings then do this:

namespace :metrics do
TEST_PATHS_FOR_RCOV = ['spec/**/*_spec.rb', 'test/**/*_test.rb']
RCOV_OPTIONS = { "--sort" => "loc" }
SAIKURO_OPTIONS = { "--warn_cyclo" => "3", "--error_cyclo" => "4" }

That's it -- hope you find it useful, lemme know if you find a bug, and check out the project home page at:

Oh, and thanks to all my co-workers who helped write the original code, in its various forms, that became this plugin.

Update 9/22/2008 - metric_fu is now a gem, on GitHub, and is useful for any Ruby Project. Check the home page for current information.

We were disposing of some last minute bugs today and I was reminded of the time, a few projects ago, when I introduced a bug that screwed up a whole release.

Let's say that I was writing a pizza ordering web site and, after many successful releases, the Big Wigs at PizzaCo wanted to introduce a new feature that would let customers customize their pizzas even more. In production the user could select style of pizza (thin, pan, stuffed), size, and toppings. There was even some cool logic in place to make sure the toppings were available before being offered. Now, in this brave new world of pizza, the customer would be given the chance to customize each topping. If you chose peppers, you would be given the choice of green, yellow, or red (through the magic of javascript). A choice of onions would prompt the question of red or yellow. Pepperoni? -- regular or spicy?

So we wrote the feature, which was difficult, as we had to totally change how we interacted with the Topping Provider Service (TPS) in order to reserve these new toppings and check for availability. But then the Big Bosses had a thought: "What if this new extra bit of choice scares away the customer?" And their solution was to have "switch" to turn the extra customization on and off. As developers we pointed out that this would be costly as making the new system look like the old, but still work with the new under the covers (by selecting defaults for each topping), was not easy. And it would introduce extra complexity by having two different pages that do almost the same thing. But they wanted it, so we did it.

A few weeks later we found a bug with the topping selection page: If you take the last bit of pepperoni, but later come back to the toppings page to change your selections, the TPS service thinks all the pepperoni is gone (because you just reserved it) so you don't see that option on the page. We fixed it by looking at the customer's saved pizza and putting any toppings missing from the page back on the page. Except that we forgot to make the changes in the page that mimics the old behavior. Even worse, the way we fixed the new toppings page caused the pseudo-old page to blow up if you ever try to go back to it. And we found out that this happens right before a release.

And this was all my fault.

I worked on the feature to add the new customization, I also worked on the feature to hide the new feature, and (believe it or not) I worked on the bug fix. There was no one else on the team who was in a better position to remember that there were two pages and each need a change. Aside from my obvious point that I'm a terrible developer and you should never hire me, I think there's an interesting lesson here about unused functionality.

We wrote the new page and then we used it. It did all sorts of sexy Ajax and was cool. And after a few weeks we forgot about the legacy page and things that might break it. There are many times when it will be tempting to bifurcate your code, but every time you do so you create a place for a bug. This is especially true if one path is rarely used. The new code will continue to evolve and the old will sit around and collect bugs. We did a full pass through regression testing, in addition to our normal tests and QA, but we didn't find this bug until hours before the release. Which is better that finding it after, but still, if the release had gone out it would have had the new feature turned off (for "safety") and the monster bug on. We were mere hours away from disaster.

I know it's hard not to want options, but every option has its price.