Dave Schweisguth in a Bottle

How many meanings of that can you think of?

Upgrading GWW from Ruby 1.8.7 to Ruby 1.9.2 and RVM

leave a comment »

Having already upgraded GWW from Rails 1 to Rails 2 and then Rails 3, the last step to bring it fully up to date was to upgrade to Ruby 1.9.2. It wasn’t difficult, but it took enough puzzling out to be worth writing down, and I even found a regression in Ruby.

Getting rspec to run

I installed rvm on my development machine per the instructions and ran the specs. They refused to run, complaining about “invalid multibyte char”s in several files. The multibyte chars were there on purpose, to test that they made it in to and out of MySQL correctly. It turns out that while Ruby 1.8.7 obligingly handled UTF-8 in source files, 1.9.2 is strict and does not. The solution is to declare that the entire source file is UTF-8 with this comment on the first line:

# encoding: UTF-8


That fixed the encoding errors, but rspec still didn’t run:

> rake spec
(in /Users/dave/data/projects/gww/git)
/Users/dave/.rvm/gems/ruby-1.9.2-p180@global/gems/rake-0.8.7/lib/rake.rb:32:
  warning: already initialized constant RAKEVERSION
/Users/dave/.rvm/gems/ruby-1.9.2-p180@global/gems/rake-0.8.7/lib/rake/alt_system.rb:32:
  warning: already initialized constant WINDOWS
WARNING: Possible conflict with Rake extension: String#ext already exists
WARNING: Possible conflict with Rake extension: String#pathmap already exists
/Users/dave/.rvm/gems/ruby-1.9.2-p180@global/gems/rake-0.8.7/lib/rake.rb:404:
  warning: already initialized constant EMPTY_TASK_ARGS
[a screenful of already initialized constants later ...]
rake aborted!
stack level too deep


These errors came from confusion among the many copies of rake installed by Apple, rvm and GWW’s Gemfile. Fortunately I’d seen some recent blog posts on the rake 0.9 debacle which reminded bundler users that we should really have been running rake with

> bundle exec rake spec


(or whatever target) all along, and indeed using bundle exec fixed the errors and allowed rspec to run. Of course some individual specs were still failing.

Getting all of the specs to pass

The most common breakage came from code like this:

describe LocationParser
  it "finds a plain address" do
    text = '555 California'
    LocationParser.new([]).parse(text).should ==
      [ Address.new text, '555', 'California', nil ]
  end
end


The spec (this example happens to be a spec, but the issue is not rspec-specific) had passed in 1.8.7, but in 1.9.2 it failed like so:

/Users/dave/.rvm/gems/ruby-1.9.2-p180/gems/rspec-core-2.5.1/lib/rspec/core/configuration.rb:386:in `load':
  /Users/dave/data/projects/gww/git/spec/lib/location_parser_spec.rb:33:
  syntax error, unexpected tIDENTIFIER, expecting ']' (SyntaxError)


The fix was simply to add parentheses around the arguments to Address.new. This seems to be a bug in Ruby 1.9.2. We’ll see what the Ruby team thinks. Meanwhile it’s easy enough to work around.

A few specs that used RR (Double Ruby) had problems. One type of problem occurred when for example I chained query methods in a controller

photos = Photo.where(:person => @person).order(:dateadded)


and stubbed it with RR’s double graphs:

stub(Photo).where.stub!.order { [ photo ] }


Specs that did this failed with

NotImplementedError: super from singleton method that is defined to multiple classes is not supported;
  this will be fixed in 1.9.3 or later


Caught red-handed! Those chained query methods didn’t belong in controllers anyway. I extracted them into model methods which I could then stub in the usual way.

The other RR problem was in a spec that stubbed the value of 1.day.ago with this RR construct

any_instance_of(ActiveSupport::Duration) do |d|
   stub(d).ago { Time.local(2011) }
end


It failed with this error:

ActionView::Template::Error: uninitialized constant ActiveSupport::Duration::RR


This only happened one place in the application so I didn’t look in to it at all. I just replaced 1.day.ago with Time.now - 1.day, stubbed Time.now and moved on.

The very last class of spec failures was an odd one. A couple of controllers converted some model objects to JSON to be inserted into a page. Their specs tested that the JSON contained what it should like so:

json = {
  'id' => photo.id,
  'latitude' => photo.latitude,
  'longitude' => photo.longitude,
  'color' => 'FFFF00',
  'symbol' => '?'
}.to_json
assigns[:json].should == json


Under 1.9.2 this spec failed, with the expected and emitted JSON Strings containing the same data but in different order. A tour of to_json, which treats hashes and ActiveRecord objects even more differently than I expected, convinced me that I wasn’t able to control the order, so I settled for this somewhat uglier alternative:

json = {
  'id' => photo.id,
  'latitude' => photo.latitude.to_s, # because BigDecimals are strings in JSON
  'longitude' => photo.longitude.to_s,
  'color' => 'FFFF00',
  'symbol' => '?'
}
ActiveSupport::JSON.decode(assigns[:json]).should == json


With that, all of the specs passed. Amusingly, almost every change I’d needed to make had been in the specs; I’d barely needed to touch the actual code.

There was one thing left to fix before moving on to production: Ruby 1.9.2 warns about suboptimal regular expressions, and GWW had one. A regexp containing something like (?:\w+)? produced this warning:

nested repeat operator + and ? was replaced with '*'


In some cases of this warning the right thing to do would be to rewrite the regexp to use * instead of (?:)? and +. In this case, the (?:)? happened to be completely unnecessary and I just removed it.

Deploying to production

Switching the production instance to 1.9.2 was easy. I just installed rvm (multi-user this time) and followed the RVM/Passenger integration instructions. In my hands Passenger does need an .rvmrc in the project root and the config/setup_load_paths.rb given in those instructions.

RVM and TeamCity

I left GWW’s continuous integration for last. GWW uses TeamCity. It has a simple three-step build: it bundle installs, it copies database.yml into place and it does rake db:migrate spec:rcov.

  • TeamCity doesn’t yet have good rvm support. To use an rvm Ruby in TeamCity, one needs to manually set in TeamCity all of the environment variables that rvm would have set. Fortunately Jonah at Carbon Five had already documented them in a nice blog post. With fake rvm in the environment the bundle install step ran without error.
  • The other issue which showed up only in TeamCity was that rake db:migrate couldn’t log in to MySQL:
    Access denied for user 'root'@'localhost' (using password: NO)


Rails was finding config/database.yml, but it was only seeing the parameters in that file’s default section, the adapter and host. That’s this bug, and the workaround was just to inline the default section. With that done CI ran green.

Code coverage

rcov runs without error on 1.9.2, but it warns that its results are probably not correct, and indeed GWW was drenched in bogus red. Switching to simplecov was as easy as possible: add one line to spec_helper.rb and bundle exec rake spec instead of bundle exec rake spec:rcov. simplecov puts its results in the same coverage directory as rcov, so nothing else had to change. As a bonus, simplecov doesn’t have a bug that rcov did (it failed to mark complex hash initializers as covered) so GWW’s coverage increased to its rightful 100%.

The benefits

I upgraded GWW to Ruby 1.9.2 not because of any improvement in the new version, but to move to the place in the Ruby open-source ecosystem which will be the best supported for the foreseeable future. Nonetheless 1.9.2 is faster than 1.8.7; GWW’s specs now run in 13 seconds instead of 18, and longer-running processes in production saw a proportional speedup.

Advertisement

Written by dschweisguth

June 3, 2011 at 16:12

Posted in Programming, Rails, Ruby

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: