diff --git a/CHANGELOG b/CHANGELOG index 9a4f624d..fff3f821 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,11 @@ Freezegun Changelog =================== +1.2.0 +----- + +* Add support for `time.perf_counter` (and `…_ns`) + 1.1.0 ----- diff --git a/README.rst b/README.rst index afc5a498..501ad16a 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. time.monotonic() will also be frozen, but as usual it makes no guarantees about its absolute value, only its changes over time. +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() and time.perf_counter() will also be frozen, but as usual it makes no guarantees about their absolute value, only their changes over time. Decorator ~~~~~~~~~ diff --git a/freezegun/api.py b/freezegun/api.py index cab9ebeb..8e6440a3 100644 --- a/freezegun/api.py +++ b/freezegun/api.py @@ -24,6 +24,7 @@ _TIME_NS_PRESENT = hasattr(time, 'time_ns') _MONOTONIC_NS_PRESENT = hasattr(time, 'monotonic_ns') +_PERF_COUNTER_NS_PRESENT = hasattr(time, 'perf_counter_ns') _EPOCH = datetime.datetime(1970, 1, 1) _EPOCHTZ = datetime.datetime(1970, 1, 1, tzinfo=dateutil.tz.UTC) @@ -31,10 +32,11 @@ real_localtime = time.localtime real_gmtime = time.gmtime real_monotonic = time.monotonic +real_perf_counter = time.perf_counter real_strftime = time.strftime real_date = datetime.date real_datetime = datetime.datetime -real_date_objects = [real_time, real_localtime, real_gmtime, real_monotonic, real_strftime, real_date, real_datetime] +real_date_objects = [real_time, real_localtime, real_gmtime, real_monotonic, real_perf_counter, real_strftime, real_date, real_datetime] if _TIME_NS_PRESENT: real_time_ns = time.time_ns @@ -44,6 +46,10 @@ real_monotonic_ns = time.monotonic_ns real_date_objects.append(real_monotonic_ns) +if _PERF_COUNTER_NS_PRESENT: + real_perf_counter_ns = time.perf_counter_ns + real_date_objects.append(real_perf_counter_ns) + _real_time_object_ids = {id(obj) for obj in real_date_objects} # time.clock is deprecated and was removed in Python 3.8 @@ -222,21 +228,51 @@ def fake_gmtime(t=None): return get_current_time().timetuple() +def _get_fake_monotonic(): + # For monotonic timers like .monotonic(), .perf_counter(), etc + current_time = get_current_time() + return ( + calendar.timegm(current_time.timetuple()) + + current_time.microsecond / 1e6 + ) + + +def _get_fake_monotonic_ns(): + # For monotonic timers like .monotonic(), .perf_counter(), etc + current_time = get_current_time() + return ( + calendar.timegm(current_time.timetuple()) * 1000000 + + current_time.microsecond + ) * 1000 + + 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 + + return _get_fake_monotonic() + + +def fake_perf_counter(): + if _should_use_real_time(): + return real_perf_counter() + + return _get_fake_monotonic() + 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 + + return _get_fake_monotonic_ns() + + +if _PERF_COUNTER_NS_PRESENT: + def fake_perf_counter_ns(): + if _should_use_real_time(): + return real_perf_counter_ns() + return _get_fake_monotonic_ns() def fake_strftime(format, time_to_format=None): @@ -638,6 +674,7 @@ def start(self): time.time = fake_time time.monotonic = fake_monotonic + time.perf_counter = fake_perf_counter time.localtime = fake_localtime time.gmtime = fake_gmtime time.strftime = fake_strftime @@ -656,6 +693,7 @@ def start(self): ('real_gmtime', real_gmtime, fake_gmtime), ('real_localtime', real_localtime, fake_localtime), ('real_monotonic', real_monotonic, fake_monotonic), + ('real_perf_counter', real_perf_counter, fake_perf_counter), ('real_strftime', real_strftime, fake_strftime), ('real_time', real_time, fake_time), ] @@ -668,6 +706,10 @@ def start(self): time.monotonic_ns = fake_monotonic_ns to_patch.append(('real_monotonic_ns', real_monotonic_ns, fake_monotonic_ns)) + if _PERF_COUNTER_NS_PRESENT: + time.perf_counter_ns = fake_perf_counter_ns + to_patch.append(('real_perf_counter_ns', real_perf_counter_ns, fake_perf_counter_ns)) + if real_clock is not None: # time.clock is deprecated and was removed in Python 3.8 time.clock = fake_clock @@ -745,6 +787,7 @@ def stop(self): time.time = real_time time.monotonic = real_monotonic + time.perf_counter = real_perf_counter time.gmtime = real_gmtime time.localtime = real_localtime time.strftime = real_strftime @@ -756,6 +799,9 @@ def stop(self): if _MONOTONIC_NS_PRESENT: time.monotonic_ns = real_monotonic_ns + if _PERF_COUNTER_NS_PRESENT: + time.perf_counter_ns = real_perf_counter_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 014e497f..9003d121 100644 --- a/tests/test_datetimes.py +++ b/tests/test_datetimes.py @@ -22,6 +22,7 @@ HAS_CLOCK = hasattr(time, 'clock') HAS_TIME_NS = hasattr(time, 'time_ns') HAS_MONOTONIC_NS = hasattr(time, 'monotonic_ns') +HAS_PERF_COUNTER_NS = hasattr(time, 'perf_counter_ns') class temp_locale(object): """Temporarily change the locale.""" @@ -59,6 +60,7 @@ def test_simple_api(): freezer.start() assert time.time() == expected_timestamp assert time.monotonic() >= 0.0 + assert time.perf_counter() >= 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) @@ -66,6 +68,7 @@ def test_simple_api(): freezer.stop() assert time.time() != expected_timestamp assert time.monotonic() >= 0.0 + assert time.perf_counter() >= 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") @@ -117,6 +120,7 @@ def test_zero_tz_offset_with_time(): assert datetime.datetime.utcnow() == datetime.datetime(1970, 1, 1) assert time.time() == 0.0 assert time.monotonic() >= 0.0 + assert time.perf_counter() >= 0.0 freezer.stop() @@ -130,6 +134,7 @@ def test_tz_offset_with_time(): assert datetime.datetime.utcnow() == datetime.datetime(1970, 1, 1) assert time.time() == 0.0 assert time.monotonic() >= 0 + assert time.perf_counter() >= 0 freezer.stop() @@ -202,27 +207,32 @@ def test_bad_time_argument(): assert False, "Bad values should raise a ValueError" -def test_time_monotonic(): +@pytest.mark.parametrize("func_name, has_func, tick_size", ( + ("monotonic", True, 1.0), + ("monotonic_ns", HAS_MONOTONIC_NS, int(1e9)), + ("perf_counter", True, 1.0), + ("perf_counter_ns", HAS_PERF_COUNTER_NS, int(1e9)),) +) +def test_time_monotonic(func_name, has_func, tick_size): initial_datetime = datetime.datetime(year=1, month=7, day=12, hour=15, minute=6, second=3) + if not has_func: + pytest.skip("%s does not exist in current version" % func_name) + with freeze_time(initial_datetime) as frozen_datetime: - monotonic_t0 = time.monotonic() - if HAS_MONOTONIC_NS: - monotonic_ns_t0 = time.monotonic_ns() + func = getattr(time, func_name) + t0 = func() 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 + + t1 = func() + + assert t1 == t0 + tick_size 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 + + t11 = func() + assert t11 == t1 + 10 * tick_size def test_time_gmtime(): @@ -677,18 +687,22 @@ def test_time_with_nested(): assert time() == second -def test_monotonic_with_nested(): - from time import monotonic +@pytest.mark.parametrize("func_name", + ("monotonic", "perf_counter") +) +def test_monotonic_with_nested(func_name): + __import__("time", fromlist=[func_name]) + invoke_time_func = lambda: getattr(time, func_name)() with freeze_time('2015-01-01') as frozen_datetime_1: - initial_monotonic_1 = time.monotonic() + initial_t1 = invoke_time_func() with freeze_time('2015-12-25') as frozen_datetime_2: - initial_monotonic_2 = time.monotonic() + initial_t2 = invoke_time_func() frozen_datetime_2.tick() - assert time.monotonic() == initial_monotonic_2 + 1 - assert time.monotonic() == initial_monotonic_1 + assert invoke_time_func() == initial_t2 + 1 + assert invoke_time_func() == initial_t1 frozen_datetime_1.tick() - assert time.monotonic() == initial_monotonic_1 + 1 + assert invoke_time_func() == initial_t1 + 1 def test_should_use_real_time(): diff --git a/tests/test_ticking.py b/tests/test_ticking.py index 75a1ed72..e29ce070 100644 --- a/tests/test_ticking.py +++ b/tests/test_ticking.py @@ -1,4 +1,5 @@ import datetime +import sys import time from unittest import mock @@ -62,12 +63,26 @@ def test_ticking_time(): assert time.time() > 1326585599.0 -@utils.cpython_only -def test_ticking_monotonic(): +@utils.cpython_only_mark +@pytest.mark.parametrize("func_name", + ("monotonic", "monotonic_ns", "perf_counter", "perf_counter_ns"), +) +def test_ticking_monotonic(func_name): + if sys.version_info[0:2] >= (3, 7): + # All of these functions should exist in Python 3.7+, so this test helps + # avoid inappropriate skipping when we've accidentally typo-ed the name + # of one of these functions 😅 + assert hasattr(time, func_name) + else: + if not hasattr(time, func_name): + pytest.skip( + "time.%s does not exist in the current Python version" % func_name) + + func = getattr(time, func_name) with freeze_time("Jan 14th, 2012, 23:59:59", tick=True): - initial_monotonic = time.monotonic() + initial = func() time.sleep(0.001) # Deal with potential clock resolution problems - assert time.monotonic() > initial_monotonic + assert func() > initial @mock.patch('freezegun.api._is_cpython', False) diff --git a/tests/utils.py b/tests/utils.py index 57b49c9d..6cb8a235 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -3,6 +3,8 @@ from freezegun.api import FakeDate, FakeDatetime, _is_cpython +import pytest + def is_fake_date(obj): return obj.__class__ is FakeDate @@ -12,6 +14,11 @@ def is_fake_datetime(obj): return obj.__class__ is FakeDatetime +cpython_only_mark = pytest.mark.skipif( + not _is_cpython, + reason="Requires CPython") + + def cpython_only(func): @wraps(func) def wrapper(*args):