Recently my colleague Henry and I ran into the old question of tests that failed because the CI server's time zone was different from ours. But we were not sure why the testers failed: we thought we had set up the tests to avoid time zones. Let's go through the process we used to find a solution ̵
The failing test was for a date propagation method on a Rails Active Record model called
arrangement ; here's how it looks:
event = Event.new event.start_time = Time.parse (& # 39; 2000-06-01 18: 00 & # 39;) expect (event.pretty_start_time) .to eq (& # 39; 6: 00 p.m. & # 39;)
We start with a timeline with
18:00 – it's 6pm. We analyze it for a while and assign it to a field on a
Event plate. Then we call a method that formats the time and confirms that it is formatted as "6:00 pm"
This works fine for us locally but fails on the CI result is "2:00 p.m." instead. So it sounds like a time zone problem. But how can it be? If the CI server is in the UTC time zone, it seems that it will analyze the string at 18:00 UTC, as the formatting will still be at 18:00. The internal representation would have a different time zone, but the formatted output would be the same. But it doesn't look like what happens.
We can recreate this problem locally by swapping the development machine's time zone to GMT (aka UTC) – on macOS, opening the date and time and then clicking somewhere in the west of Africa and it should set it for you.
When we set our development machine to UTC, we get "2 pm" as the formatted output. Why is this happening?
In Rail's apps, there is a difference between the time of the system and the time of your Rails program. System time respects the time zone to which the current machine is set. The application time is adjusted to a time zone that can be set to run time or configured. In our case, in
config / application.rb it was set to Eastern:
config.time_zone = & # 39; Eastern Time (US and Canada) & # 39;
So this explains the difference. The system time is UTC and the input "18:00" is analyzed as 18:00 UTC. Then the exit tries to send it out using the application time zone in the east, 18:00 UTC is 2pm eastern or "14:00"
It 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.
Time.parse as we do in the test: > Time.parse (& # 39; 2000-06-01 18: 00 & # 39;)
=> 2000-06-01 18:00:00 +0000
> Time.parse (& 06-06-2004 18:00 & # 39;). Class
Time.parse returns a regular Ruby
Time instance, which obviously does not know about the Rails & # 39; time zone configuration. So it uses the system time. What if we call
Time.zone.parse instead, with the system's time zone set to UTC?
> Time.zone.parse (& # 39; 2000-06-01 18: 00 & # 39;) => Thu, 01 Jun 2000 18:00:00 EDT -04: 00 > Time.zone.parse (& 06-06-2004 18: 00 & # 39;). Class => ActiveSupport :: TimeWithZone
Time.zone.parse returns a
ActiveSupport :: TimeWithZone example. From the return value, we can see that the instance has its time zone set to the Time Zone for Rails application
EDT . And it interprets the analysis "18:00" as 18:00 in the time zone.
With this knowledge, let's look back at the code we currently use in the error testing test. We assign a regular Ruby
Time to an Active Record instance. What happens then?
> event = Event.new * Snip * > event.start_time = Time.parse (& # 39; 2000-06-01 18: 00 & # 39;) => 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 outbound result values that we assign a regular Ruby
Time to the field
start_time but when we retrieve it, it is a
ActiveSupport :: TimeWithZone with the Eastern Time Zone. Moreover, the original time of 18:00 UTC is converted to 14:00 East.
So this is where the link happens. A system time comes in and an application time comes out. It makes sense that a Rails-Managed Active Record instance will be standardized to use Rails
ActiveSupport :: TimeWithZone class, which is aware of the configuration of time zone configuration of Rails.
Now we know everything we need to know to fix the error. If we start with an application time initially using
Time.zone it should remain the same all the way through:
> event.start_time = Time.zone.parse (& # 39; 2000- 06 -01 18:00 & # 39;) => Thu, 01 Jun 2000 18:00:00 EDT -04: 00 > event.start_time => Tue, 01 Jun 2000 18:00:00 EDT -04: 00
And since it is 18:00 in the eastern time zone, the formatting method returns what we expect:
> event.pretty_start_time => "6:00 pm"
Interestingly, we can even just assign the string to the field and let Rails automatically manage to parse it to a
ActiveSupport :: TimeWithZone !
> event.start_time = & # 39; 2000-06-01 18: 00 & # 39; => "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 submissions usually work. When a user enters a time in a form, we do not explicitly analyze the string string. Rails handle it for us in that case too.
When we change our test to use
Time.zone.parse it even passes with local or CI server time set to UTC:
event = Event.new event.start_time = Time.zone.parse (& # 39; 2000-06-01 18: 00 & # 39;) expect (event.pretty_start_time) .to eq (6:00 pm & # 39;)
So it is takeaway: because Rails operates on application time, it is best for you to consistently use
Time.zone in console and tests so expectations are not erroneous. Or maybe just work with strings directly!
In this post, we do not know whether it is good to set the program time zone to a specific time zone, or to UTC, or to allow users to set their time zone as a preference. No matter what approach you take,
Time.zone consistently uses inconsistencies between times you make and times Rails create for you.
Big thanks to Henry Harris for all his help in troubleshooting this problem!