Repository: adamchainz/time-machine

Issue:
Handle windows epoch
`time.time()` and other functions in `time()` return seconds-since-epoch, but that epoch is not 1 Jan 1970 on windows but [1 Jan 1601](https://stackoverflow.com/a/8670712/1427135) ([wikipedia](https://en.wikipedia.org/wiki/Epoch_(computing)#Notable_epoch_dates_in_computing)). So functions like `time.time()` need updating to offset based on the system epoch, which the python docs say can be retrieved with `time.gmtime(0)`.

Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/adamchainz/time-machine/pull/98?src=pr&el=h1) Report
> Merging [#98](https://codecov.io/gh/adamchainz/time-machine/pull/98?src=pr&el=desc) (1d02532) into [master](https://codecov.io/gh/adamchainz/time-machine/commit/71cf08aa78b14b756348eb3beaa476d271ec07cb?el=desc) (71cf08a) will **decrease** coverage by `4.69%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/adamchainz/time-machine/pull/98/graphs/tree.svg?width=650&height=150&src=pr&token=X0GKFD0HT5)](https://codecov.io/gh/adamchainz/time-machine/pull/98?src=pr&el=tree)

```diff
@@             Coverage Diff             @@
##            master      #98      +/-   ##
===========================================
- Coverage   100.00%   95.30%   -4.70%     
===========================================
  Files            1        1              
  Lines          209      213       +4     
  Branches        38       38              
===========================================
- Hits           209      203       -6     
- Misses           0        5       +5     
- Partials         0        5       +5     
```


| [Impacted Files](https://codecov.io/gh/adamchainz/time-machine/pull/98?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/time\_machine.py](https://codecov.io/gh/adamchainz/time-machine/pull/98/diff?src=pr&el=tree#diff-c3JjL3RpbWVfbWFjaGluZS5weQ==) | `95.30% <100.00%> (-4.70%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/adamchainz/time-machine/pull/98?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/adamchainz/time-machine/pull/98?src=pr&el=footer). Last update [71cf08a...1d02532](https://codecov.io/gh/adamchainz/time-machine/pull/98?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index e1e2c8f..dc83381 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -11,6 +11,7 @@ History
 * Remove ``tz_offset`` argument. This was incorrectly copied from
   ``freezegun``. Use the new timezone mocking with ``ZoneInfo`` instead.
 * Add pytest plugin and fixture ``time_machine``.
+* Work with Windows’ different epoch.
 
 1.3.0 (2020-12-12)
 ------------------
diff --git a/src/time_machine.py b/src/time_machine.py
index 9a36961..ed2cfb2 100644
--- a/src/time_machine.py
+++ b/src/time_machine.py
@@ -4,6 +4,7 @@ import inspect
 import os
 import sys
 import uuid
+from time import gmtime as orig_gmtime
 from time import tzset
 from types import GeneratorType
 from typing import Optional
@@ -33,6 +34,22 @@ except ImportError:  # pragma: no cover
 
 NANOSECONDS_PER_SECOND = 1_000_000_000
 
+# Windows' time epoch is not unix epoch but in 1601. This constant helps us
+# translate to it.
+_system_epoch = orig_gmtime(0)
+SYSTEM_EPOCH_TIMESTAMP_NS = int(
+    dt.datetime(
+        _system_epoch.tm_year,
+        _system_epoch.tm_mon,
+        _system_epoch.tm_mday,
+        _system_epoch.tm_hour,
+        _system_epoch.tm_min,
+        _system_epoch.tm_sec,
+        tzinfo=dt.timezone.utc,
+    ).timestamp()
+    * NANOSECONDS_PER_SECOND
+)
+
 
 def extract_timestamp_tzname(destination):
     if callable(destination):
@@ -82,14 +99,15 @@ class Coordinates:
         if not self._tick:
             return self._destination_timestamp_ns
 
+        base = SYSTEM_EPOCH_TIMESTAMP_NS + self._destination_timestamp_ns
         now_ns = self._time_ns()
 
         if not self._requested:
             self._requested = True
             self._real_start_timestamp_ns = now_ns
-            return self._destination_timestamp_ns
+            return base
 
-        return self._destination_timestamp_ns + (now_ns - self._real_start_timestamp_ns)
+        return base + (now_ns - self._real_start_timestamp_ns)
 
     if sys.version_info >= (3, 7):
 


Test patch:
diff --git a/tests/test_time_machine.py b/tests/test_time_machine.py
index 3721d45..8f6aa5f 100644
--- a/tests/test_time_machine.py
+++ b/tests/test_time_machine.py
@@ -4,7 +4,7 @@ import sys
 import time
 import uuid
 from importlib.util import module_from_spec, spec_from_file_location
-from unittest import SkipTest, TestCase
+from unittest import SkipTest, TestCase, mock
 
 import pytest
 from dateutil import tz
@@ -249,6 +249,25 @@ def test_time_time():
     assert now >= LIBRARY_EPOCH
 
 
+windows_epoch_in_posix = -11_644_445_222
+
+
+@mock.patch.object(
+    time_machine,
+    "SYSTEM_EPOCH_TIMESTAMP_NS",
+    (windows_epoch_in_posix * NANOSECONDS_PER_SECOND),
+)
+def test_time_time_windows():
+    with time_machine.travel(EPOCH):
+        first = time.time()
+        assert isinstance(first, float)
+        assert first == windows_epoch_in_posix
+
+        second = time.time()
+        assert isinstance(second, float)
+        assert windows_epoch_in_posix < second < windows_epoch_in_posix + 1.0
+
+
 def test_time_time_no_tick():
     with time_machine.travel(EPOCH, tick=False):
         assert time.time() == EPOCH