Just wanted to put out a note of caution to other developers. Any code (test/spec or production) that depends on global environment settings or non-deterministic functions needs to be treated with fear and trembling. As a concrete example of this maxim, code that deals with Time, Date, and DateTime has the possibility to trip you up with both global environment and non-determinism. I recently discovered a potential bug related to this… but it took me 45 minutes to track down… so I wrote up the following afterwards:
nota bene: some of the methods described herein are not defined by Ruby’s core library’s but by Rails’
ActiveSupport.
Any non-determinism in tests is dangerous, and Time.now and its close cousins are certainly on that list. Whether the code which depends on the current date is in the production code or in the spec code, we’ll need to be very careful of it, lest it trip us up.
I’ve known this for a long time… but I just spent about 45 minutes perplexed with a particularly stupid bug that simply required an attentive eye to unravel… I’ll gladly share my debugging session (condensed for time and minus the aggravation and going in circles), and hopefully save someone else the hassle:
>> DateTime.yesterday < DateTime.now => true >> DateTime.tomorrow < DateTime.now => true
Well that shouldn’t ever happen. Confused yet? Or do you already know where this is going? I spent a great deal of time wondering if it was some weird interaction between the Date, Time, and DateTime classes (and trust me, those weird interactions do exist, although maybe they weren’t the cause of this particular bug).
>> DateTime.tomorrow => Sun, 24 Aug 2008 >> DateTime.yesterday => Fri, 22 Aug 2008 >> DateTime.now => Sat, 23 Aug 2008 23:24:35 -0400
Ummm… nothing that would explain the comparison results above… but that’s curious… DateTime.now gives a different inspect output than the others. I wonder why?
>> DateTime.yesterday.class => Date >> DateTime.tomorrow.class => Date >> DateTime.now.class => DateTime
Intriguing. Certainly not what I expected, given that all of these values were obtained from class methods on DateTime. Perhaps this means that the comparison requires some coercion to work… I didn’t investigate the source to see if it uses “to_datetime” as opposed to “to_time”, but I’m assuming that it does, because of the following:
>> DateTime.tomorrow.to_time < DateTime.now.to_time => false >> DateTime.tomorrow.to_datetime < DateTime.now.to_datetime => true
So what does Date#to_datetime do to screw us over?
>> DateTime.yesterday.to_datetime => Fri, 22 Aug 2008 00:00:00 0000 >> DateTime.tomorrow.to_datetime => Sun, 24 Aug 2008 00:00:00 0000 >> DateTime.now.to_datetime => Sat, 23 Aug 2008 23:28:11 -0400
Do you see it? The answer is right there, but it’s easy to miss. I missed it over and over again for about 40 minutes. ”-0400” — the timezone. The timezone had been screwing me up all along. The DateTime object has my timezone from the get-go, but the Date objects were converted as UTC. Lovely… just lovely.
Now, I wasn’t the original author of the code. Just an archaeologist. My guess: the code worked when it was built (during business hours, not in the wee hours of the night when the timezones would trip them up), and it looked like the simplest thing needed. Generally, you really can’t fault a developer for going that route. I certainly don’t. But times and dates and especially anything related to the current time and date… well, these are special animals, and need to be dealt with special care.
The long and short of it? Timezones are global environment state, and you need to be aware of it with any code (especially tests) that deal with time. Avoid DateTime.yesterday or DateTime.tomorrow like the plague in your test/spec code (unless you enjoy tests that spuriously fail near the witching hour). You should write your tests (and your production code) in such a way that you can simulate different timezones and different times of day in order to tease out bugs. And if you need to compare or convert dates and times in your production code, be very aware of what time and timezone your business rules will require, especially if you have any Date objects that might be automatically coerced into DateTime objects.