timecop
timecop copied to clipboard
TimeCop handles timezones incorrectly
I am not sure why this issue was closed but I am following up with the suggestion to allow for handling of UTC dates. If this cannot happen by default, perhaps it can be done with an option of sorts.
In our case, the current logic leads to specs that pass during the day and fail during the night.
Here is an example:
it 'returns all reports generated on a certain date' do
Timecop.freeze 1.day.ago do
create_list :report, 4, name: '1.day.ago'
end
Report.generated_on(1.day.ago).count.should == 4
end
The intent of the spec couldn't be clearer. It is difficult for me to agree with any default logic in Timecop that leads to inconsistent behavior in such a simple case.
Since I did not want to patch the gem without further discussion, my workaround was external. It only works well for dates.
# Timecop does not handle UTC well
# Adjust dates to a time that will be in the same date in both UTC and local TZ
def safe_date(t)
tz_offset = Time.now.gmt_offset
if tz_offset != 0
t.beginning_of_day - (tz_offset + (tz_offset / tz_offset.abs) * 1.minute)
else
t
end
end
it 'returns all reports generated on a certain date' do
Timecop.freeze safe_date(1.day.ago) do
create_list :report, 4, name: '1.day.ago'
end
Report.generated_on(safe_date(1.day.ago)).count.should == 4
end
I don't really understand your issue, so can you take your solution and make an actual patch we can look at?
My solution doesn't actually fix Timecop's behavior. Are you suggesting I submit it as a class-level helper method on Timecop or as a patch to Date?
@ssimeonov I'd be interested to see the code that is used for your Report#generated_on and your create_list helper, to see if either of those is in some way normalizing the Time objects involved (e.g., converting them to Date objects).
Your workaround is not applying an offset (which would create an object that has the same unix timestamp), but rather adding/subtracting seconds from t.beginning_of_day (aka, changing the time by the offset, which has a side effect of ensuring that the shifted timestamp's #to_date yields the matching UTC day).
Also, for debugging purposes, could you provide your current time zone?
@yaauie there is no Time normalization code in Report#generated_on or create_list, other than calling #to_date at some point but that's due to core business logic and not anything to do with normalization per se. Beyond that, your description of my workaround is exactly right, which is why I don't see it as a great solution, even though it is TZ-agnostic.
My TZ is EST with DST observation.
@ssimeonov:
Without trying to understand the purported bug, I wouldn't write a test like this:
it 'returns all reports generated on a certain date' do
Timecop.freeze 1.day.ago do
create_list :report, 4, name: '1.day.ago'
end
Report.generated_on(1.day.ago).count.should == 4
end
You call 1.day.ago twice and expect them to match up. If the first one runs a millisecond before midnight and the last one a millisecond after midnight, the example will fail.
You could put both inside the block or use a variable, maybe something like:
it 'returns all reports generated on a certain date' do
date = Date.yesterday
Timecop.freeze(date) do
create_list :report, 4, name: 'whatever'
end
Report.generated_on(date).count.should == 4
end
But also, your fix seems suspect, though I don't quite understand your issue.
We would have to see your implementation of generated_on.
Dates and time zones can be a bit tricky, since dates without time can't be converted between zones.
Timecop.freeze("2012-05-24".to_date) do
assert_equal "2012-05-24", Date.today.to_s
end
This test fails depending on your system TZ. It doesn't seems right. Timecop.freeze(DATE) should behave different from Timecop.freeze(TIME) since dates are not affected by tzs?
@irregular You're right, there seems to be a bug or unexpected behavior there.
I tried this in RSpec:
date = Date.new(2012, 5, 24)
Timecop.freeze(date) do
p ENV['TZ']
p Time.now
Date.today.to_s.should == "2012-05-24"
end
The output is: "PST" 2012-05-23 22:00:00 +0000
Our tests set ENV['TZ'] = "PST" but the machine local time and the Rails Time.zone are CET.
The value of Time.now corresponds to midnight on the start of 2012-05-24 in CET, but the output is shown as UTC.
If I do
date = Date.new(2012, 5, 24)
p date.to_time
in the same test, the output is
2012-05-24 00:00:00 +0000
by the way.
@irregular @henrik Timecop only mocks Time methods, so rather than using Date.today use Time.now.to_date, even if Timecop's handling of timezones were perfect you would still have issues there.
I had a similar issue with datetime comparisons after adding Timecop, the above solution seems to have worked. Here's how we changed our global before(:each) for the suite:
now = ::Time.now.utc
::Time.stub!(:now).and_return(now)
- date_time_now = ::DateTime.now.utc
- ::DateTime.stub!(:now).and_return(date_time_now)
+ ::DateTime.stub!(:now).and_return(now.to_datetime)
I believe some of the issues discussed here are resolved here : https://github.com/travisjeffery/timecop/pull/73 (use of Date.today on some passed dates to freeze/travel)
1.day.ago is the equivalent of Time.now - 1.day, which is timezone-lossy; that means this issue is not a Timecop issue and a red herring:
> Time.zone = 'Hawaii' # because it's a different date than my computer's ENV['TZ'] UTC right now
> 1.day.ago
#=> 2013-07-17 09:02:50 +0000
> Time.now - 1.day
#=> 2013-07-17 09:02:56 +0000
> Time.zone.now - 1.day
#=> Tue, 16 Jul 2013 23:05:42 HST -10:00
My best guess would be that somewhere in your Report.generated_on method, you're going through the current Time.zone to get to a date (e.g., Time.zone.at(timestamp).to_date), while the report's generated_on date just gets set with Date.today or Time.now.to_date (or vice versa); either would cause the drift you're experiencing, but both are outside the hands of Timecop. Because #to_date is also a lossy conversion, either every instance of it must go through Time.zone or none of them may.
> Time.zone = 'Hawaii' # same reason as above
> Date.today
#=> Thu, 18 Jul 2013
> Time.now.to_date
#=> Thu, 18 Jul 2013
> Time.zone.now.to_date
#=> Wed, 17 Jul 2013
Interesting that 1.day.ago isn't Time.zone aware. Didn't expect that.
+1
I just face this same issue...
@bcardiff if you'll follow the comments, you'll see that I believe this not to be a Timecop issue, but rather an issue in the code under test. If you could supply additional information that proves me wrong, I would be glad to dig into it some more.
@yaauie I just extract a little example of what I face some minutes ago. Apologize me if I'm wrong. But it was kind of weird the issue. I put a repo at https://github.com/bcardiff/timecop_issue Model would be https://github.com/bcardiff/timecop_issue/blob/master/app/models/foo.rb Spec would be https://github.com/bcardiff/timecop_issue/blob/master/spec/models/foo_spec.rb
The 1 & 2 are failing, and the 3 is passing. The only difference is the Timecop.freeze initialization.
Maybe this should fall more on TimeWithZone since, if the foo.reload are removed, all specs pass. (I've just discovered that :-$). But either way, it was kind of unexpected that the first test fail.
@bcardiff you aren't failing due to unexpected timezone drift, so your bug is not this one.
EDIT: Your bug has been fixed since the 'v0.5.3' Timecop in your Gemfile.lock. Please upgrade bundle update --source timecop :smile:
I'm getting the following output, which is consistent with what you noted above:
Failures:
1) Foo should mark with current time 2
Failure/Error: foo.time_field.should eq(Time.zone.now)
expected: Thu, 18 Jul 2013 19:36:40 UTC +00:00
got: Thu, 18 Jul 2013 19:36:40 UTC +00:00
(compared using ==)
Diff:
# ./spec/models/foo_spec.rb:20:in `block (3 levels) in <top (required)>'
# ./spec/models/foo_spec.rb:15:in `block (2 levels) in <top (required)>'
2) Foo should mark with current time
Failure/Error: foo.time_field.should eq(Time.zone.now)
expected: Thu, 18 Jul 2013 19:36:40 UTC +00:00
got: Thu, 18 Jul 2013 19:36:40 UTC +00:00
(compared using ==)
Diff:
# ./spec/models/foo_spec.rb:10:in `block (3 levels) in <top (required)>'
# ./spec/models/foo_spec.rb:5:in `block (2 levels) in <top (required)>'
Finished in 0.02797 seconds
3 examples, 2 failures
Worth noting is that those timestamps look correct, but they're failing anyway. This has to do with a loss of precision somewhere in the underlying storage of the Time object, but is hidden because their #to_s representation looks the same. If you compare the float values expect { foo.time_field.to_f }.to eq(Time.zone.now.to_f), you will see more clearly that there is precision loss, which is a different bug. Your Date-frozen one passes (most of the time, see #100) because a Date's starting timestamp doesn't have sub-second precision.
@yaauie I see. Thanks for clarifying. I verified it was just a precision issue. using Time.now.change(sec: 0) or Date works nicely in the specs. I appreciate your time.
@bcardiff (see my edit above. you can just upgrade to the newest Timecop since the precision issue has been fixed)
I'm under GMT-3 and the other developer is under GMT-4. We are using VCR to record some requests with the timestamp in the querystring (HMAC).
If we use Timecop.freeze it doesn't preserve my timezone, raising VCR errors VCR::Errors::UnhandledHTTPRequestError.
Here is an example:
context 'using Timecop.freeze' do
before do
now = Time.new(2013, 8, 16, 10, 55, 14, '-03:00')
Timecop.freeze(now)
end
it 'requests ... ' do
now = Time.now
# WRONG, it should be -0300
# => 2013-08-16 09:55:14 -0400
end
end
context 'using Time.stub' do
before do
now = Time.new(2013, 8, 16, 10, 55, 14, '-03:00')
Time.stub(now: now)
end
it 'requests ... ' do
now = Time.now
# it is correct ;)
=> 2013-08-16 10:55:14 -0300
end
end
+1
I'm trying to test something in every time zone to make sure it has the same output.
Output of the times before calling Timecop.freeze:
testing 2014-10-30 14:02:02 -1200
testing 2014-10-30 15:02:02 -1100
testing 2014-10-30 16:02:02 -1000
...
Output of the times when calling Time.now from Timecop.freeze block:
testing 2014-10-30 22:02:02 -0400
testing 2014-10-30 22:02:02 -0400
testing 2014-10-30 22:02:02 -0400
...
> Time.now
=> 2014-09-29 10:06:24 +0000
> Date.today
=> Mon, 29 Sep 2014
> Date.today.beginning_of_month
=> Mon, 01 Sep 2014
> Timecop.freeze Date.today.beginning_of_month
=> 2014-08-31 22:00:00 +0000
The expected date is 2014-09-01.
I'm not sure if this is the same problem, but my timezone is CEST (+2), so it's
probably related to this (even though Time.now gives +0000?)....
So, this thread seems to be on and off, but I have found that our tests are failing based on which timezone it is run on.
This works just fine EST.
> Timecop.freeze('2013-11-20 14:05:16 -0500')
=> "2013-11-20T14:05:16.000-05:00"
But in IST, it's fast forward 10.5 hours -- likely due to how TimeZone is calculated from GMT/UTC. -05:00 EST is most definitely 10.5 hours ahead in IST.
> Timecop.freeze('2013-11-20 14:05:16 -0500')
=> "2013-11-21T00:35:16.000+05:30"
I'm not convinced that we have the correct format for tests or anything (we inherited this app), but it's certainly causing issues for having developers in multiple countries across multiple timezones.
We used dotenv and dotenv-rails to load in a .env in the test environment that set TZ=America/New_York. This let us keep application configuration the same in the codebase, but set the "system" time zone to the zone the tests expected.
Our tests were failing on CI because our Rails config.time_zone was configured to one thing, but the CI machine was configured to something different. Calling Time.zone.now returned the Rails configured timezone, but inside the Timecop.freeze block, the time was for the system timezone.
I was able to work around this problem by changing the system timezone of our CI machine.
I believe I am experiencing these issues as well, currently trying to resolve issues regarding mixed use of Date, Time, and ActiveSupport::TimeWithZone first before submitting some code relating to timecop - however, I think there might be an issue between my local dev machine's system TZ and the TZ Timecop uses.
Yes, freezing Timecop creates an issue where a timezone is set that is actually not set anywhere within the application or system. ActiveSupport thinks its UTC (correct) while Timecop thinks its PDT.
@stratigos i'm having a similar issue with ActiveSupport::TimeWithZone where it's converts to UTC but takes into the calculation the actual system timezone. So it gives me 2 different dates :(
So sad.
+1
I'm having an issue with differences time zones too.
My Rails application has set timezone: Eastern Time (US & Canada)
when I create record and pass to it mocked time the time zone of record set as EDT but time zone of mocked time is GMT+3. GMT+3 is my time zone where I am it's Russia.
Rails 5.0.2 Ruby 2.3.1 Timecop 0.8.1
I bypassed it by mocking value of record that is expected in my test.
before do
allow(model).to receive(:last_sign_in_at).and_return(mocked_time)
end
@IlkhamGaysin have you seen https://github.com/travisjeffery/timecop/issues/182 ?