From 8938a6913f12d64b483694ef0df3fb83d5fba165 Mon Sep 17 00:00:00 2001 From: Julian Mehnle Date: Wed, 30 Sep 2020 17:53:30 +0000 Subject: [PATCH] =?UTF-8?q?Add=20support=20for=20`time.monotonic`=20(and?= =?UTF-8?q?=20`=E2=80=A6=5Fns`)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AUTHORS.rst | 1 + README.rst | 16 ++++++++++++++-- freezegun/api.py | 35 +++++++++++++++++++++++++++++++++- tests/test_datetimes.py | 42 +++++++++++++++++++++++++++++++++++++++++ tests/test_ticking.py | 8 ++++++++ 5 files changed, 99 insertions(+), 3 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 01c19293..41bdaabd 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -16,3 +16,4 @@ Patches and Suggestions - `James Lu `_ - `Dan Elkis `_ - `Bastien Vallet `_ +- `Julian Mehnle `_ diff --git a/README.rst b/README.rst index 3b0c783d..a33a8b2a 100644 --- a/README.rst +++ b/README.rst @@ -13,7 +13,7 @@ FreezeGun is a library that allows your Python tests to travel through time by m Usage ----- -Once the decorator or context manager have been invoked, all calls to datetime.datetime.now(), datetime.datetime.utcnow(), datetime.date.today(), time.time(), time.localtime(), time.gmtime(), and time.strftime() will return the time that has been frozen. +Once the decorator or context manager have been invoked, all calls to datetime.datetime.now(), datetime.datetime.utcnow(), datetime.date.today(), time.time(), time.localtime(), time.gmtime(), and time.strftime() will return the time that has been frozen. time.monotonic() will also be frozen, but as usual it makes no guarantees about its absolute value, only its changes over time. Decorator ~~~~~~~~~ @@ -174,7 +174,7 @@ FreezeGun allows for the time to be manually forwarded as well. .. code-block:: python - def test_manual_increment(): + def test_manual_tick(): initial_datetime = datetime.datetime(year=1, month=7, day=12, hour=15, minute=6, second=3) with freeze_time(initial_datetime) as frozen_datetime: @@ -188,6 +188,18 @@ FreezeGun allows for the time to be manually forwarded as well. initial_datetime += datetime.timedelta(seconds=10) assert frozen_datetime() == initial_datetime +.. code-block:: python + + def test_monotonic_manual_tick(): + initial_datetime = datetime.datetime(year=1, month=7, day=12, + hour=15, minute=6, second=3) + with freeze_time(initial_datetime) as frozen_datetime: + monotonic_t0 = time.monotonic() + frozen_datetime.tick(1.0) + monotonic_t1 = time.monotonic() + assert monotonic_t1 == monotonic_t0 + 1.0 + + Moving time to specify datetime ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/freezegun/api.py b/freezegun/api.py index 738016d6..1c69d8e5 100644 --- a/freezegun/api.py +++ b/freezegun/api.py @@ -22,21 +22,27 @@ MayaDT = None _TIME_NS_PRESENT = hasattr(time, 'time_ns') +_MONOTONIC_NS_PRESENT = hasattr(time, 'monotonic_ns') _EPOCH = datetime.datetime(1970, 1, 1) _EPOCHTZ = datetime.datetime(1970, 1, 1, tzinfo=dateutil.tz.UTC) real_time = time.time real_localtime = time.localtime real_gmtime = time.gmtime +real_monotonic = time.monotonic real_strftime = time.strftime real_date = datetime.date real_datetime = datetime.datetime -real_date_objects = [real_time, real_localtime, real_gmtime, real_strftime, real_date, real_datetime] +real_date_objects = [real_time, real_localtime, real_gmtime, real_monotonic, real_strftime, real_date, real_datetime] if _TIME_NS_PRESENT: real_time_ns = time.time_ns real_date_objects.append(real_time_ns) +if _MONOTONIC_NS_PRESENT: + real_monotonic_ns = time.monotonic_ns + real_date_objects.append(real_monotonic_ns) + _real_time_object_ids = {id(obj) for obj in real_date_objects} # time.clock is deprecated and was removed in Python 3.8 @@ -211,6 +217,23 @@ def fake_gmtime(t=None): return get_current_time().timetuple() +def fake_monotonic(): + if _should_use_real_time(): + return real_monotonic() + current_time = get_current_time() + return calendar.timegm(current_time.timetuple()) + current_time.microsecond / 1000000.0 + +if _MONOTONIC_NS_PRESENT: + def fake_monotonic_ns(): + if _should_use_real_time(): + return real_monotonic_ns() + current_time = get_current_time() + return ( + calendar.timegm(current_time.timetuple()) * 1000000 + + current_time.microsecond + ) * 1000 + + def fake_strftime(format, time_to_format=None): if time_to_format is None: if not _should_use_real_time(): @@ -609,6 +632,7 @@ def start(self): datetime.date = FakeDate time.time = fake_time + time.monotonic = fake_monotonic time.localtime = fake_localtime time.gmtime = fake_gmtime time.strftime = fake_strftime @@ -626,6 +650,7 @@ def start(self): ('real_datetime', real_datetime, FakeDatetime), ('real_gmtime', real_gmtime, fake_gmtime), ('real_localtime', real_localtime, fake_localtime), + ('real_monotonic', real_monotonic, fake_monotonic), ('real_strftime', real_strftime, fake_strftime), ('real_time', real_time, fake_time), ] @@ -634,6 +659,10 @@ def start(self): time.time_ns = fake_time_ns to_patch.append(('real_time_ns', real_time_ns, fake_time_ns)) + if _MONOTONIC_NS_PRESENT: + time.monotonic_ns = fake_monotonic_ns + to_patch.append(('real_monotonic_ns', real_monotonic_ns, fake_monotonic_ns)) + if real_clock is not None: # time.clock is deprecated and was removed in Python 3.8 time.clock = fake_clock @@ -710,6 +739,7 @@ def stop(self): setattr(module, module_attribute, real) time.time = real_time + time.monotonic = real_monotonic time.gmtime = real_gmtime time.localtime = real_localtime time.strftime = real_strftime @@ -718,6 +748,9 @@ def stop(self): if _TIME_NS_PRESENT: time.time_ns = real_time_ns + if _MONOTONIC_NS_PRESENT: + time.monotonic_ns = real_monotonic_ns + if uuid_generate_time_attr: setattr(uuid, uuid_generate_time_attr, real_uuid_generate_time) uuid._UuidCreate = real_uuid_create diff --git a/tests/test_datetimes.py b/tests/test_datetimes.py index d1d76762..014e497f 100644 --- a/tests/test_datetimes.py +++ b/tests/test_datetimes.py @@ -21,6 +21,7 @@ # time.clock was removed in Python 3.8 HAS_CLOCK = hasattr(time, 'clock') HAS_TIME_NS = hasattr(time, 'time_ns') +HAS_MONOTONIC_NS = hasattr(time, 'monotonic_ns') class temp_locale(object): """Temporarily change the locale.""" @@ -57,12 +58,14 @@ def test_simple_api(): freezer.start() assert time.time() == expected_timestamp + assert time.monotonic() >= 0.0 assert datetime.datetime.now() == datetime.datetime(2012, 1, 14) assert datetime.datetime.utcnow() == datetime.datetime(2012, 1, 14) assert datetime.date.today() == datetime.date(2012, 1, 14) assert datetime.datetime.now().today() == datetime.datetime(2012, 1, 14) freezer.stop() assert time.time() != expected_timestamp + assert time.monotonic() >= 0.0 assert datetime.datetime.now() != datetime.datetime(2012, 1, 14) assert datetime.datetime.utcnow() != datetime.datetime(2012, 1, 14) freezer = freeze_time("2012-01-10 13:52:01") @@ -113,6 +116,7 @@ def test_zero_tz_offset_with_time(): assert datetime.datetime.now() == datetime.datetime(1970, 1, 1) assert datetime.datetime.utcnow() == datetime.datetime(1970, 1, 1) assert time.time() == 0.0 + assert time.monotonic() >= 0.0 freezer.stop() @@ -125,6 +129,7 @@ def test_tz_offset_with_time(): assert datetime.datetime.now() == datetime.datetime(1969, 12, 31, 20) assert datetime.datetime.utcnow() == datetime.datetime(1970, 1, 1) assert time.time() == 0.0 + assert time.monotonic() >= 0 freezer.stop() @@ -197,6 +202,29 @@ def test_bad_time_argument(): assert False, "Bad values should raise a ValueError" +def test_time_monotonic(): + initial_datetime = datetime.datetime(year=1, month=7, day=12, + hour=15, minute=6, second=3) + with freeze_time(initial_datetime) as frozen_datetime: + monotonic_t0 = time.monotonic() + if HAS_MONOTONIC_NS: + monotonic_ns_t0 = time.monotonic_ns() + + frozen_datetime.tick() + monotonic_t1 = time.monotonic() + assert monotonic_t1 == monotonic_t0 + 1.0 + if HAS_MONOTONIC_NS: + monotonic_ns_t1 = time.monotonic_ns() + assert monotonic_ns_t1 == monotonic_ns_t0 + 1000000000 + + frozen_datetime.tick(10) + monotonic_t11 = time.monotonic() + assert monotonic_t11 == monotonic_t1 + 10.0 + if HAS_MONOTONIC_NS: + monotonic_ns_t11 = time.monotonic_ns() + assert monotonic_ns_t11 == monotonic_ns_t1 + 10000000000 + + def test_time_gmtime(): with freeze_time('2012-01-14 03:21:34'): time_struct = time.gmtime() @@ -649,6 +677,20 @@ def test_time_with_nested(): assert time() == second +def test_monotonic_with_nested(): + from time import monotonic + + with freeze_time('2015-01-01') as frozen_datetime_1: + initial_monotonic_1 = time.monotonic() + with freeze_time('2015-12-25') as frozen_datetime_2: + initial_monotonic_2 = time.monotonic() + frozen_datetime_2.tick() + assert time.monotonic() == initial_monotonic_2 + 1 + assert time.monotonic() == initial_monotonic_1 + frozen_datetime_1.tick() + assert time.monotonic() == initial_monotonic_1 + 1 + + def test_should_use_real_time(): frozen = datetime.datetime(2015, 3, 5) expected_frozen = 1425513600.0 diff --git a/tests/test_ticking.py b/tests/test_ticking.py index d7e2e749..75a1ed72 100644 --- a/tests/test_ticking.py +++ b/tests/test_ticking.py @@ -62,6 +62,14 @@ def test_ticking_time(): assert time.time() > 1326585599.0 +@utils.cpython_only +def test_ticking_monotonic(): + with freeze_time("Jan 14th, 2012, 23:59:59", tick=True): + initial_monotonic = time.monotonic() + time.sleep(0.001) # Deal with potential clock resolution problems + assert time.monotonic() > initial_monotonic + + @mock.patch('freezegun.api._is_cpython', False) def test_pypy_compat(): try: