timecop icon indicating copy to clipboard operation
timecop copied to clipboard

TimeCop handles timezones incorrectly

Open ssimeonov opened this issue 13 years ago • 32 comments
trafficstars

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.

ssimeonov avatar Sep 18 '12 02:09 ssimeonov

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

ssimeonov avatar Sep 18 '12 02:09 ssimeonov

I don't really understand your issue, so can you take your solution and make an actual patch we can look at?

travisjeffery avatar Sep 18 '12 03:09 travisjeffery

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 avatar Sep 18 '12 05:09 ssimeonov

@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 avatar Sep 27 '12 06:09 yaauie

@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 avatar Sep 27 '12 06:09 ssimeonov

@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

henrik avatar Jan 04 '13 09:01 henrik

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.

henrik avatar Jan 04 '13 09:01 henrik

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?

old-grrt avatar Jan 26 '13 18:01 old-grrt

@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.

henrik avatar Jan 26 '13 19:01 henrik

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.

henrik avatar Jan 26 '13 19:01 henrik

@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.

travisjeffery avatar Jan 27 '13 16:01 travisjeffery

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)

readysetawesome avatar Feb 25 '13 20:02 readysetawesome

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)

tommeier avatar Mar 08 '13 02:03 tommeier

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

yaauie avatar Jul 18 '13 09:07 yaauie

Interesting that 1.day.ago isn't Time.zone aware. Didn't expect that.

henrik avatar Jul 18 '13 17:07 henrik

+1

I just face this same issue...

bcardiff avatar Jul 18 '13 17:07 bcardiff

@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 avatar Jul 18 '13 18:07 yaauie

@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 avatar Jul 18 '13 19:07 bcardiff

@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 avatar Jul 18 '13 19:07 yaauie

@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 avatar Jul 18 '13 20:07 bcardiff

@bcardiff (see my edit above. you can just upgrade to the newest Timecop since the precision issue has been fixed)

yaauie avatar Jul 18 '13 20:07 yaauie

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 

phstc avatar Oct 23 '13 01:10 phstc

+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
...

WattsInABox avatar May 14 '14 20:05 WattsInABox

> 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?)....

arp242 avatar Sep 30 '14 08:09 arp242

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.

lauram-spindance avatar Dec 17 '14 19:12 lauram-spindance

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.

jamesmartin avatar Feb 06 '15 12:02 jamesmartin

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 avatar Jul 25 '16 15:07 stratigos

@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.

maximveksler avatar Oct 09 '16 21:10 maximveksler

+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 avatar May 04 '17 16:05 IlkhamGaysin

@IlkhamGaysin have you seen https://github.com/travisjeffery/timecop/issues/182 ?

ballcheck avatar May 08 '17 10:05 ballcheck