I just wrote about the imminent release of my new gem rical which is a fresh implementation of the RFC 2445 (iCalendar) specification for Ruby.
Almost as an afterthought, I decided to look at what it would take to make the gem work with Ruby 1.8.6, 1.8.7 and 1.9.1. I hadnât really thought about 1.9 compatibility while working on rical, so I didnât know how much work would be needed.
Iâd seen that David Chelimsky had said that
a new version of RSpec which ran on Ruby 1.9 was imminent but that was over a month ago and I hadnât heard an update, but David responded quickly to my email (using his iPhone while stopped at a traffic light!) that yes that rspec 1.2.4 does indeed work with 1.9, and I was off to the races.
Executive Summary
It turns out that making rical compatible with Ruby 1.9 was fairly easy. The problems fell into a few areas:
- Ruby 1.9 has made DateTime parsing less flexible.
- Ruby 1.9 Range#include? requires the range to be iterable
- Hash enumeration order is different
Multiruby
The first step was to set up multiruby. Iâd used multiruby before but not in a while. Actually I had just looked at it again, because the last few alpha drops of Maglev have required a specific patchlevel of Ruby 1.8.6 and ParseTree. Maglev currently relies on ParseTree to turn Ruby source code into an s-expression which it then turns into objects and extended Gemstone Smalltalk byte codes. Eventually this will be replaced by a native compiler of some sort.
I figured that the easiest way to get this version was via multiruby, but I was having troubles getting Maglev to work with it. It couldnât find the Parsetree gem. It turned out that I had a back level of ZenTest (which contains multiruby along with autotest and a lot of other useful stuff). But I got tied up most of yesterday with the Radiant sprint, so I didnât get things straightened out until I got home yesterday evening.
I updated zentest, blew away the .multiruby directory in my home directory, and reinstalled the âusualâ and ruby gems
multiruby_setup the_usual
multiruby_setup update:rubygems
Now I could start to try to run my specs under 1.8.6, 1.8.7, and 1.9.
This led to a series of missing gems, first rspec. Multiruby by default has a gem directory for each version of ruby which is separate from your normal gem directory. Itâs possible to share these, but going with the path of least resistance, I just installed each missing gem as I discovered it. For example to install rspec for all the multiruby installed versions:
multiruby -S gem install rspec
I used the bones gem to setup the rical gem. When I installed this gem I ran into a hitch. Although the rical gem doesnât need it for deployment, the bones gem (like many of itâs other brother gem developer assistants) adds rake tasks which depend on other gems. Since the bones provided rakefile defined a spec:rcov task which requires the rcov gem, which doesnât seem to be 1.9 compatible yet, I got to a point where although the specs would run on 1.8.6 and 1.8.7, the rakefile (or maybe it was a task file) was preventing rake from initializing under 1.9.
At this point I took advantage of the separate gem directories under the .multiruby directory structure and hacked the copy of the bones gem under the 1.9 install to remove the need for rcov, since I was really only interested in whether the specs ran on 1.9.
The Issues
As I said, I didnât know what to expect in terms of compatibility. The spec suite for rical has 550 spec examples.
As it turns out, although there were lots of failures, they fell into large clumps.
DateTime.parse differences
In Ruby 1.8 the DateTime.parse method uses heuristics which attempt to figure out whether certain strings represent dates in different local formats. For example, here in the US a date like â12/8/2001â is interpreted as December 8, 2001, while in most of Europe it would be August 12, 2001. Iâve gotten in the personal habit of writing such a date as 8 December 2001, which uses the European order, but is also understandable to my countrymen.
I wrote many spec examples directly from the recurrence rule use cases in the RFC2445 spec which tend to use American style dates in the document, mm/dd/yyyy. The Ruby 1.8 datetime seems to get these right.
Ruby 1.9 dropped this and always interprets nn/nn/nnnn as dd/mm/yyyy, which broke LOTs of my spec examples.
Luckily the breakage was all in the specs, not in the rical code under test. So a bit of regex replacement magic with Textmate to reformat the test input data, and lots of spec examples now ran under 1.9 as well as 1.8.x.
Range#include? Difference
In the recurrence rule enumeration code I had a method:
def in_outer_cycle?(candidate)
candidate && (outer_range.nil? || outer_range.include?(candidate))
end
The outer_range attribute is a Range of a starting and ending DateTime.
In Ruby 1.8.x Range#include? checks to see if the argument is >= the start of the range, and either <= or < the end of the range if the range is inclusive or exclusive.
In Ruby 1.9, was getting an error like âcanât iterate from DateTime.â Range#include? now actually works more like Array#include? and steps through the elements of the range.
The fix in this case was to change the method to:
def in_outer_cycle?(candidate)
candidate && (outer_range.nil? || (outer_range.first <= candidate && outer_range.last >= candidate))
end
Hash enumeration order difference
In Ruby 1.9 hashes keep track of the order of key insertion, and when a hash is enumerated it yields the keys or values or key value pairs in that order.
In Ruby 1.8 the enumeration order is accidental and depends on the key hash values and whether or not there are key collisions.
My last problem was a spec which read:
it "should properly format dtstart with a date-time with a local time zone" do
@it.dtstart = date_time_with_tzinfo_zone(DateTime.parse("4/22/2009 17:55"), "America/New_York")
@it.export.should match(/^DTSTART;TZID=America\/New_York;X-RICAL-TZSOURCE=TZINFO;VALUE=DATE-TIME:20090422T175500$/)
One thing that Iâd already change was the string argument to DateTime.parse. But that wasnât enough.
The match expectation was failing under 1.9 because the substrings, â;TZID=America/NewYorkâ, â;X-RICAL-TZSOURCE=TZINFOâ, and â;VALUE=DATE-TIMEâ were appearing in a different order under Ruby 1.9 than 1.8.x.
This was coming from a method:
# Return a string representing the receiver in RFC 2445 format
def to_s
if visible_params && !visible_params.empty?
"#{visible_params.map {|key, val| ";#{key}=#{val}"}}:#{value}"
else
":#{value}"
end
end
The order of those sub-pieces depends on the enumeration order of visible_params which is a hash.
I left this last problem overnight, to sleep on.
This example is over-specified. The order of those three substrings doesnât really matter. Now I could write a somewhat more complicated spec example, but pragmatically I decided to leave it over-specified but specify an order which I could guarantee under all ruby versions. So the to_s method became:
# Return a string representing the receiver in RFC 2445 format
def to_s
# We only sort for testability reasons
if (vp = visible_params) && !vp.empty?
"#{vp.keys.sort.map {|key| ";#{key}=#{vp[key]}"}.join}:#{value}"
else
":#{value}"
end
end
And the example changed slightly to:
it "should properly format dtstart with a local time zone" do
@it.dtstart = date_time_with_tzinfo_zone(DateTime.parse("April 22, 2009 17:55"), "America/New_York")
@it.export.should match(/^DTSTART;TZID=America\/New_York;VALUE=DATE-TIME;X-RICAL-TZSOURCE=TZINFO:20090422T175500$/)
end
Note the change to the Ruby 1.9 friendly âApril 22, 2009â¦â and the re-arrangement of the substrings in the regular expression.
So after I got the multiruby setup straight, it really only took an hour or two to make rical 1.9 compatible.