Software Maintainability in New Technologies
Leveling UpEvery decade at the longest, the software development industry undergoes significant technological shifts, and these shifts make it difficult to keep delivering maintainable software....
5 min read
Jul 19, 2019
Recently my coworker Henry and I ran into the old issue of tests failing because the CI server’s time zone was different from ours. But we weren’t sure why the tests were failing: we thought we had set up the tests to avoid time zone issues. Let’s walk through the process we used to find a fix–and more importantly, find out why the fix works.
The failing test was for a date formatting method on a Rails Active Record model named Event
; here’s what it looks like:
event = Event.new event.start_time = Time.parse('2000-06-01 18:00') expect(event.pretty_start_time).to eq('6:00 p.m.')
We start with a time string with 18:00
—that is, 6 pm. We parse it into a time and assign it to a field on an Event
record. Then we call a method that formats the time and confirm it’s formatted as “6:00 p.m.”
This works fine for us locally, but fails on CI—the result is “2:00 p.m.” instead. So that certainly sounds like a time zone issue. But how can it be? If the CI server is in the UTC time zone, it seems like it would parse the string as 6 PM UTC, then when formatting it it’d still be 6 PM. The internal representation would have a different time zone, but the formatted output would be the same. But that doesn’t seem to be what’s happening.
We can reproduce this problem locally by changing our development machine’s time zone to GMT (aka UTC)—on macOS, open the Date & Time settings then click somewhere in the western part of Africa and that should set it for you.
When we set our development machine to UTC, we get “2 p.m.” as the formatted output. Why does this happen?
In Rails apps, there’s a difference between your system’s time and your Rails application’s time. System time respects whatever time zone the current machine is set to. Application time is aligned to a time zone that can be set at runtime or configured. In our case, in config/application.rb
, it was set to Eastern:
config.time_zone = 'Eastern Time (US & Canada)'
So this explains the difference. The system time is UTC, and the input “18:00” is parsed as 18:00 UTC. Then the output attempts to output it using the application time zone of Eastern, 18:00 UTC is 14:00 Eastern, or “2:00 p.m.”
That is a step in the right direction, but why is system time used for parsing and application time used for formatting? And what can we do to fix it in the test? Rails’ Time.zone
method can help us–let’s see how and why by pulling up the Rails console.
Let’s call Time.parse
as we do in the test:
> Time.parse('2000-06-01 18:00') => 2000-06-01 18:00:00 +0000 > Time.parse('2000-06-01 18:00').class => Time
So Time.parse
returns a plain Ruby Time
instance, which of course doesn’t know about Rails’ time zone configuration. So it’s using the system time. What if we call Time.zone.parse
instead, with our system time zone set to UTC?
> Time.zone.parse('2000-06-01 18:00') => Thu, 01 Jun 2000 18:00:00 EDT -04:00 > Time.zone.parse('2000-06-01 18:00').class => ActiveSupport::TimeWithZone
So Time.zone.parse
returns an ActiveSupport::TimeWithZone
instance. From the return value, we can see that the instance has its time zone set to the Rails application time zone of EDT
. And it interprets the parsed “18:00” as 6 pm in that time zone.
With this knowledge, let’s look back at the code we’re currently using in the failing test. We’re assigning a plain Ruby Time
to an Active Record instance. What happens then?
> event = Event.new *snip* > event.start_time = Time.parse('2000-06-01 18:00') => 2000-06-01 18:00:00 +0000 > event.start_time => Thu, 01 Jun 2000 14:00:00 EDT -04:00 > event.start_time.class => ActiveSupport::TimeWithZone
We can tell from the outputted result values that we are assigning a plain Ruby Time
to the start_time
field, but when we retrieve it back, it’s an ActiveSupport::TimeWithZone
with the Eastern time zone. What’s more, the original 18:00 UTC has been converted to 14:00 Eastern.
So this is where the disconnect is happening. A system time is coming in and an application time is coming out. It makes sense that a Rails-managed Active Record instance would be standardized to use Rails’ ActiveSupport::TimeWithZone
class, which is aware of the Rails application time zone configuration.
Now we know all we need to know to fix the bug. If we start with an application time in the first place by using Time.zone
, it should stay the same all the way through:
> event.start_time = Time.zone.parse('2000-06-01 18:00') => Thu, 01 Jun 2000 18:00:00 EDT -04:00 > event.start_time => Thu, 01 Jun 2000 18:00:00 EDT -04:00
And since it’s 18:00 in the Eastern time zone, the formatting method returns what we expect:
> event.pretty_start_time => "6:00 p.m."
Interestingly, we can even just assign the string to the field and let Rails automatically handle parsing it into an ActiveSupport::TimeWithZone
!
> event.start_time = '2000-06-01 18:00' => "2000-06-01 18:00" > event.start_time => Thu, 01 Jun 2000 18:00:00 EDT -04:00 > event.start_time.class => ActiveSupport::TimeWithZone
This behavior makes sense because of how form submission usually works. When a user enters a time in a form we aren’t explicitly parsing that string value; Rails is handling it for us in that case as well.
When we change our test to use Time.zone.parse
, it passes even with local or CI server time set to UTC:
event = Event.new event.start_time = Time.zone.parse('2000-06-01 18:00') expect(event.pretty_start_time).to eq('6:00 p.m.')
So there’s the takeaway: because Rails operates on application time, it’s best for you to consistently use Time.zone
in the console and tests so that expectations don’t get mismatched. Or maybe even just work with strings directly!
In this post we’re not getting into whether it’s a good thing to set the application time zone to a specific time zone, or to UTC, or to let users set their time zone as a preference. Whichever approach you take, using Time.zone
consistently should prevent mismatches between times you create and times Rails creates for you.
Big thanks to Henry Harris for all his help troubleshooting this issue!
Every decade at the longest, the software development industry undergoes significant technological shifts, and these shifts make it difficult to keep delivering maintainable software....
Where is the Ruby language headed? At RubyConf 2021, the presentations on the language focused on type checks and performance—where performance can be subdivided...
At RubyConf 2021, TypeProf-IDE was announced. It's a Visual Studio Code integration for Ruby’s TypeProf tool to allow real-time type analysis and developer feedback....