Ruby debugging with puts, tap and Hirb
I use puts heaps when debugging. Combined with tap, it’s pretty handy. You can jump right in the middle of a method chain without having to move things around into variables.
1 |
x = long.chain.of.methods.tap {|x| puts x }.to.do.something.with
|
I thought hey why don’t I merge the two? And for bonus points, add in Hirb’s table display to format my models nicely. These are fairly personal customizations, and aren’t specific to a project, so I put them in my own ~/.railsrc file rather than each project.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# config/initializers/developer_specific_customizations.rb if %w(development test).include?(Rails.env) railsrc = "#{ENV['HOME']}/.railsrc" load(railsrc) if File.exist?(railsrc) end # ~/.railsrc require 'hirb' Hirb.enable :pager => false class Object def tapp(prefix = nil, &block) block ||= lambda {|x| x } tap do |x| value = block[x] value = Hirb::View.formatter.format_output(value) || value.inspect if prefix print prefix if value.lines.count > 1 print ":\n" else print ": " end end puts value end end end # Usage (in your spec files, perhaps?) "hello".tapp # => hello "hello".tapp('a') # => a - "hello "hello".tapp(&:length) # => 5 MyModel.first.tapp # => # +----+-------------------------+ # | id | created_at | # +----+-------------------------+ # | 7 | 2009-12-29 00:15:56 UTC | # +----+-------------------------+ # 1 row in set |
BacktraceCleaner and gems in rails
UPDATE: Fixed the monkey-patch to match the latest version of the patch, and to explicitly require Rails::BacktraceCleaner before patching it to make sure it has been loaded
If there’s one thing my mother taught me, if you’re going to clean something up you may as well do it properly. Be thorough, cover every surface.
Rails::BacktraceCleaner is a bit sloppy when it comes to gem directories. It misses all sorts of dust – hyphens, underscores, upper case letters, numbers. That’s not going to earn any pocket money. Let’s teach it a lesson.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# config/initializers/this_is_what_a_gem_looks_like.rb require 'rails/backtrace_cleaner' module Rails class BacktraceCleaner < ActiveSupport::BacktraceCleaner private GEM_REGEX = "([A-Za-z0-9_-]+)-([0-9.]+)" def add_gem_filters Gem.path.each do |path| # http://gist.github.com/30430 add_filter { |line| line.sub(/(#{path})\/gems\/#{GEM_REGEX}\/(.*)/, '\2 (\3) \4')} end vendor_gems_path = Rails::GemDependency.unpacked_path.sub("#{RAILS_ROOT}/",'') add_filter { |line| line.sub(/(#{vendor_gems_path})\/#{GEM_REGEX}\/(.*)/, '\2 (\3) [v] \4')} end end end |
I’ve submitted a patch to rails, please review if you like.
Kudos to Matthew Todd for pairing with me on this.
Acts_as_state_machine locking
consider the following!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class Door < ActiveRecord::Base acts_as_state_machine :initial => :closed state :closed state :open, :enter => :say_hello event :open do transitions :from => :closed, :to => :open end def say_hello puts "hello" end end door = Door.create! fork do transaction do door.open! end end door.open! # >> hello # >> hello |
It’s broken, you can only open a door once. This is a classic double-update problem. One way to solve is with pessimistic locking. I made some codes that automatically lock any object when you call an event on it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class ActiveRecord::Base # Forces all state transition events to obtain a DB lock def self.obtain_lock_before_all_state_transitions event_table.keys.each do |transition| define_method("#{transition}_with_lock!") do self.class.transaction do lock! send("#{transition}_without_lock!") end end alias_method_chain "#{transition}!", :lock end end end class Door < ActiveRecord::Base # ... as before obtain_lock_before_all_state_transitions end |
beware! Your state transitions can now throw ActiveRecord::RecordNotFound errors (from lock!), since the object may have been deleted before you got a chance to play with it.
If you’re not using any locking in your web app, you’re probably doing it wrong. Just sayin’.
Selenium, webrat and the firefox beta
*UPDATE: * Latest version of webrat (0.5.3, maybe earlier) includes a fixed version of selenium, so you shouldn’t need this hack.
I needed a few hacks to get selenium running with webrat.
First, make sure you are running at least 0.4.4 of webrat. Don’t make the same mistake I did and upgrade your gem version, but not the plugin installed in vendor/plugins.
1 2 3 |
gem install webrat gem install selenium-client gem install bmabey-database_cleaner --source=http://gems.github.com |
There is a trick to get Firefox 3.5 beta working. The selenium server package with webrat 0.4.4 only supports FF 3.0.*. Follow these instructions, patching the jar that is packaged with webrat (vendor/selenium-server.jar) so that the extensions that selenium uses will be valid for the new FF.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
cd vendor/ # In webrat dir
jar xf selenium-server.jar \
customProfileDirCUSTFFCHROME/extensions/readystate@openqa.org/install.rdf
jar xf selenium-server.jar \
customProfileDirCUSTFFCHROME/extensions/{538F0036-F358-4f84-A764-89FB437166B4}/install.rdf
jar xf selenium-server.jar \
customProfileDirCUSTFFCHROME/extensions/\{503A0CD4-EDC8-489b-853B-19E0BAA8F0A4\}/install.rdf
jar xf selenium-server.jar \
customProfileDirCUSTFF/extensions/readystate\@openqa.org/install.rdf
jar xf selenium-server.jar \
customProfileDirCUSTFF/extensions/\{538F0036-F358-4f84-A764-89FB437166B4\}/install.rdf
replace "3.0.*" "3.*" -- `find . | grep rdf`
jar uf selenium-server.jar \
customProfileDirCUSTFFCHROME/extensions/readystate@openqa.org/install.rdf
jar uf selenium-server.jar \
customProfileDirCUSTFFCHROME/extensions/{538F0036-F358-4f84-A764-89FB437166B4}/install.rdf
jar uf selenium-server.jar \
customProfileDirCUSTFFCHROME/extensions/\{503A0CD4-EDC8-489b-853B-19E0BAA8F0A4\}/install.rdf
jar uf selenium-server.jar \
customProfileDirCUSTFF/extensions/readystate\@openqa.org/install.rdf
jar uf selenium-server.jar \
customProfileDirCUSTFF/extensions/\{538F0036-F358-4f84-A764-89FB437166B4\}/install.rdf
|
(hat tip to space vatican)
I haven’t been able to get Safari working yet.
I want to run selenium tests besides normal webrat tests, so I created a new environment “acceptance” that I can run tests under. Modify your test helper file:
1 2 3 4 5 6 7 8 |
# test/test_helper.rb ENV["RAILS_ENV"] ||= "test" raise "Can't run tests in #{ENV['RAILS_ENV']} environment" unless %w(test acceptance).include?(ENV["RAILS_ENV"]) require 'webrat' require "test/env/#{ENV["RAILS_ENV"]}" # ... |
1 2 3 4 5 6 7 |
# test/env/test.rb require 'webrat/rails' Webrat.configure do |config| config.mode = :rails config.open_error_files = false end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# test/env/acceptance.rb require 'webrat/selenium/silence_stream' require 'webrat/selenium' require 'test/selenium_helpers' require 'test/element_helpers' # Required because we aren't isolating tests inside a transaction require 'database_cleaner' DatabaseCleaner.strategy = :truncation Webrat.configure do |config| config.mode = :selenium end class ActionController::IntegrationTest self.use_transactional_fixtures = false # Necessary, otherwise selenium will never see any changes! setup do |session| session.host! "localhost:3001" end teardown do DatabaseCleaner.clean end end # Hack: webrat requires this, even though we're not using rspec module Spec module Expectations class ExpectationNotMetError < Exception end end end |
1 2 3 4 5 6 7 8 9 10 11 |
# lib/tasks/test.rake namespace :test do task :force_acceptance do ENV["RAILS_ENV"] = 'acceptance' end Rake::TestTask.new(:acceptance => :force_acceptance) do |t| t.test_files = FileList['test/acceptance/*_test.rb'] t.verbose = true end end |
Notes
- selenium and javascript helpers are from pivotallabs pat, they’re really handy for testing visibilty of DOM elements
- there’s some magic in webrat to conditionally require
silence_streambased on something in active_support. I don’t understand it quite enough, but requiring it explicitly was necessary to get things running for me - webrat/selenium assumes some classes are loaded that only happens if you’re using rspec. I’m not, so stubbed out the ExpectationNotMetError (it is only referred to in a rescue block).
rake test:acceptanceruns the selenium tests. Running acceptance tests directly as a ruby script runs them using normal webrat – this is actually handy when writing tests because you get a quicker turnaround.- to pause selenium mid test run (to see wtf is going on), just add
getsat the appropriate line in your test
Faster rails testing with ruby_fork
A long running test suite isn’t the problem. Your build server can take care of that. A second or two here or there, no one notices.
The killer wait is in the red/green/refactor loop. You’re only running one or two tests, and an extra second can mean the difference between getting into flow or switching to twitter. And you know what kills you in rails?
1 2 3 4 5 |
$ time ruby -e '' -r config/environment.rb real 0m3.784s user 0m2.707s sys 0m0.687s |
Yep, the environment. That’s a lot of overhead to be waiting for everytime you run a test, especially since it’s the same code every time! You fix this with a clever script called ruby_fork that’s included in the ZenTest package. It loads up your environment, then just chills out, waiting. You send a ruby file to it, and it forks itself (the process containing the environment) to execute that file. The beauty of this is that forking is really quick, and it leaves a pristine copy of the environment around for the next test run.
‘Environment’ doesn’t just have be environment.rb, for bonus points you can load up test_helper.rb, which will also load your testing framework into memory. In fact, you can preload any ruby code at all – ruby_fork isn’t rails specific.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$ ruby_fork -r test/test_helper.rb & /opt/local/bin/ruby_fork Running as PID 526 on 9084 $ time ruby_fork_client -r test/unit/your_test.rb Started ... Finished in 0.565636 seconds. # Aside: this time is bollocks 3 tests, 4 assertions, 0 failures, 0 errors real 0m0.972s # This is the time you're interested in user 0m0.225s sys 0m0.035s |
That’s fantastic, though you’ll notice in newer versions of rails your application code is not reloaded. By default your test environment caches classes – which normally isn’t a problem except that newer rails versions also eager load those classes (so they’re loaded when you load enviornment.rb). You can fix this by clearing out the eager load paths in your test environment file:
1 2 |
# config/environments/test.rb
config.eager_load_paths = []
|
On my machine this gets individual test runs down from about 4 seconds to less than 1 second. You can sell that to your boss as a four-fold productivity increase.
Backup MySQL to S3 with Rails
Here is some code I wrote over the weekend – db2s3. It’s a rails plugin that provides rake tasks for backing up your database and storing it on Amazon’s S3 cloud storage. S3 is a trivially cheap offsite backup solution – for small databases it costs about 4 cents per month, even if you’re sending full backups every hour.
There are many scripts around that do this already, but they fail to address the biggest actual problem. The aws-s3 gem provides a really nice ruby interface to S3, and dumping a backup then storing it really isn’t that hard. The real problem is that I really hate system administration. I want to spend as little time as possible and I want things to Just Work.
A script is great but there’s still too many things for me to do. Where does it go in my project? How do I set my credentials? How do I call it?
That’s why a plugin was needed. It’s as little work as possible for a rails developer to backup their database, so they can get back to making their app awesome.
db2s3. Check it out.
Singleton resource, pluralized controller fix for rails
map.resource still looks for a pluralized controller. This has always bugged me. Here’s a quick monkey patch to fix. Tested on rails 2.2.2.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# config/initializers/singleton_resource_fix.rb module ActionController module Resources class SingletonResource < Resource #:nodoc: def initialize(entity, options) @singular = @plural = entity # options[:controller] ||= @singular.to_s.pluralize options[:controller] ||= @singular.to_s # This is the only line to change super end end end end |
1 2 3 4 5 6 |
# config/routes.rb # before fix map.resource :session, :controller => 'sessions' # after fix map.resource :session |
Integration testing with Cucumber, RSpec and Thinking Sphinx
Ideally you would want to include sphinx in your integration tests. It’s really just like your database. In practice, this is problematic. Ensuring the DB is started and triggering a re-index after each model load is doable, if slow, with a small bit of hacking of thinking sphinx (hint – change the initializer for the ThinkingSphinx::Configuration to allow you to specify the environment). Here’s the rub though – if you’re using transactional fixtures the sphinx indexer won’t be able to see any of your data! Turning that off can really slow down your tests, and once you add in the re-indexing time you’re going to be making a few cups of coffee while they run.
One approach I’ve been taking is to stub out the search methods with RR. I know, I know, stubbing in your integration tests is evil. I’m being pragmatic here. For most applications your search is trivial (find me results for this keyword), and if you unit test your define_index block you’re pretty well covered. To go one step further you could unit test your controllers with an expect on the search method, or have a separate suite of non-transactional integration tests running against sphinx. I like the latter, but haven’t done it yet.
Enough talk! Here’s the magic you need to get it working with cucumber:
1 2 3 4 5 6 7 8 9 |
# features/steps/env.rb require 'rr' Cucumber::Rails::World.send(:include, RR::Adapters::RRMethods) # features/steps/*_steps.rb Given /a car with model '(\w+)' exists/ do |model| car = Car.create!(:model => model) stub(Car).search(model) { [car] } end |
Speeding up Rails Initialization
Chad Wooley just posted a tip to get rails starting up faster. Which is real, except it doesn’t work if you’re using ActiveScaffold. This is due to a load ordering problem – ActiveScaffold monkey patches the Resource class used by routes after routes have been parsed the first time, and relies on the re-parsing triggered by the inflections change.
To fix this, you can explicitly require the monkey patch just before you draw your routes (it doesn’t depend on anything else in ActiveScaffold).
1 2 3 4 5 6 7 |
# config/routes.rb ActionController::Routing::Routes.draw do |map| # Explicitly require this, otherwise it won't get loaded before we parse our resources time require 'vendor/plugins/active_scaffold/lib/extensions/resources.rb' # Your routes go here... end |
Yes it’s a hack on top of hack, but I get my console 30% quicker, so I’m running with it.
Tested on 2.0.2
Testing flash.now with RSpec
flash.now has always been a pain to test. The the traditional rails approach is to use assert_select and find it in your views. This clearly doesn’t work if you want to test your controller in isolation.
Other folks have found work arounds to the problem, including mocking out the flash or monkey patching it.
These solutions feel a bit like using a sledgehammer to me. If you’re going to monkey patch/mock something, you want it to be as discreet as possible so to minimize the chance of the implementation changing underneath you and also to reduce the affect on other areas of your application. Also, why duplicate perfectly good code that is provided elsewhere?
The real problem with testing flash.now is that it gets cleaned up (via #sweep) at the end of the action before you get to test anything. So let’s solve that problem and that problem only: disable sweeping of flash.now:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# spec/spec_helper.rb module DisableFlashSweeping def sweep end end # A spec describe BogusController, "handling GET to #index" do it "sets flash.now[:message]" do @controller.instance_eval { flash.extend(DisableFlashSweeping) } get :index flash.now[:message].should_not be_nil end end |
instance_eval is used to access the flash, since it’s a protected method, and we extend with the minimum possible code to do what we want – blanking out the sweep method. This should not cause problems because sweeping is only relevant across multiple requests, which we shouldn’t be doing in our controller specs.
Unobtrusive live comment preview with jQuery
Live preview is shiny. First get your self a URL that renders a comment. In rails maybe something like the following.
1 2 3 4 5 6 7 8 9 |
def new @comment = Comment.build_for_preview(params[:comment]) respond_to do |format| format.js do render :partial => 'comment.html.erb' end end end |
Now you should have a form or div with an ID something like “new_comment”. Just drop in the following JS (you may need to customize the submit_url).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
$(function() { // onload
var comment_form = $('#new_comment')
var input_elements = comment_form.find(':text, textarea')
var submit_url = '/comments/new'
var fetch_comment_preview = function() {
jQuery.ajax({
data: comment_form.serialize(),
url: submit_url,
timeout: 2000,
error: function() {
console.log("Failed to submit");
},
success: function(r) {
if ($('#comment-preview').length == 0) {
comment_form.after('<h2>Your comment will look like this:</h2><div id="comment-preview"></div>')
}
$('#comment-preview').html(r)
}
})
}
input_elements.keyup(function () {
fetch_comment_preview.only_every(1000);
})
if (input_elements.any(function() { return $(this).val().length > 0 }))
fetch_comment_preview();
})
|
The only_every function is they key to this piece – it ensures that an AJAX request will be sent at most only once a second so you don’t overload your server or your client’s connection.
Obviously you’ll need jQuery, less obviously you’ll also need these support functions
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Based on http://www.germanforblack.com/javascript-sleeping-keypress-delays-and-bashing-bad-articles
Function.prototype.only_every = function (millisecond_delay) {
if (!window.only_every_func)
{
var function_object = this;
window.only_every_func = setTimeout(function() { function_object(); window.only_every_func = null}, millisecond_delay);
}
};
// jQuery extensions
jQuery.prototype.any = function(callback) {
return (this.filter(callback).length > 0)
}
|
Viola, now you’re shimmering in awesomeness. Demo up soon, but it’s similar to what you see on this blog (though this blog is done with inline prototype).
AtomFeedHelper produces invalid feeds
Summary: atom_feed is broken until changeset 8529
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# http://api.rubyonrails.org/classes/ActionView/Helpers/AtomFeedHelper.html#M000931 atom_feed do |feed| feed.title("My great blog!") feed.updated((@posts.first.created_at)) for post in @posts feed.entry(post) do |entry| entry.title(post.title) entry.content(post.body, :type => 'html') entry.author do |author| author.name("DHH") end end end end |
Produces the following feed (rails 2.0.2)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?xml version="1.0" encoding="UTF-8"?> <feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom"> <id>tag:localhost:posts</id> <link type="text/html" rel="alternate" href="http://localhost:3000"/> <title>My great blog!</title> <updated>2007-12-23T04:23:07+11:00</updated> <entry> <id>tag:localhost:3000:Post1</id> <published>2007-12-23T04:23:07+11:00</published> <updated>2007-12-30T15:29:55+11:00</updated> <link type="text/html" rel="alternate" href="http://localhost:3000/posts/1"/> <title>First post</title> <content type="html">Check out the first post</content> <author> <name>DHH</name> </author> </entry> </feed> |
Let’s run that through the feed validator
1 2 3 |
line 3, column 25: id is not a valid TAG line 2, column 0: Missing atom:link with rel="self" line 8, column 32: id is not a valid TAG |
Oh dear. Not a happy result. Let’s fix it.
Problem the first is the feed ID tag. It doesn’t include a date, as per the Tag URI specification. This is a little bit tricky – you can’t just add Time.now.year as a default because that will change every year, and we need IDs to stay the same. We will provide an option to the user to specify the schema date, and produce a warning if they do not (as much as I’d like to just break it, the pragmatic side of me keeps backwards compatibility in).
The entry tag has the same problem, but you’ll also note it concatenates the class and the ID with no separator to create the ID. While it’s an edge case, this will break if you have a class name ending in a number, so we need to add in a separator. I vote for a slash. Also, the port in the tag URI is inconsistent with the feed URI (no port), so remove it.
For further reading, I recommend How to make a good ID in Atom.
The missing self link is just your garden variety bug – the documentation says it should be provided by default, but the code does not.
I went ahead and fixed these problems. Changeset 8529. The example above, when you change the call to atom_feed(:schema_date => 2008), looks like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?xml version="1.0" encoding="UTF-8"?> <feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom"> <id>tag:localhost:/posts</id> <link type="text/html" rel="alternate" href="http://localhost:3000"/> <link type="application/atom+xml" rel="self" href="http://localhost:3000/posts.atom"/> <title>My great blog!</title> <updated>2007-12-23T04:23:07+11:00</updated> <entry> <id>tag:localhost:Post/1</id> <published>2007-12-23T04:23:07+11:00</published> <updated>2007-12-30T15:29:55+11:00</updated> <link type="text/html" rel="alternate" href="http://localhost:3000/posts/1"/> <title>First post</title> <content type="html">HOORAY. About ruby.</content> <author> <name>DHH</name> </author> </entry> </feed> |
mmm, semantic goodness
Test setup broken in Rails 2.0.2
Some changes went into rails 2.0.2 that mean the setup method in test subclasses won’t get called. Here’s how it went down:
You can see some code illustrating the problem in 8445. This affects two plugins that we’re using – helper_test and activemessaging.
For the helper test, the work around is to rename your helper test setup methods to setup_with_fixtures.
1 2 3 |
def setup_with_fixtures super end |
For activemessaging, add the following line to the setup of your functionals that are failing (from the mailing list):
1 |
ActiveMessaging.reload_activemessaging
|
Rails devs, reclaim your harddrive
1 2 |
cd code-dir find . | egrep "(development|test)\\.log" | grep -v .svn | xargs rm |
I’d forgotten to clear out my logs for a long while. This found me 9.5Gb!
Logging SQL statistics in rails
When your sysadmin comes to you whinging with a valid concern that your app is reading 60 gazillion records from the DB, you kinda wish you had a bit more information than % time spent in the DB. So I wrote a plugin that counts both the number of selects/updates/inserts/deletes and also the number of records affected.
1 |
git clone git://github.com/xaviershay/sql-counter.git vendor/plugins/sql_counter |
That does the counting, you need to decide how to log it. I am personally quite partial to adding it to the request log line, thus getting stats per request:
1 2 3 4 5 |
# vendor/rails/actionpack/lib/action_controller/benchmarking.rb:75 log_message << " | Select Records: #{ActiveRecord::Base.connection.select_record_count}" log_message << " | Selects: #{ActiveRecord::Base.connection.select_count}" ActiveRecord::Base.connection.reset_counters! |
Don’t forget the last line, otherwise you get cumulative numbers. That may be handy, but I doubt it. We’re only logging selects because that’s all we care about at the moment. I am sure this will change in time.
UPDATE: Moved to github, bzr repo is no longer available