Skip to content

Commit

Permalink
Merge pull request #395 from pganssle/perf_counter
Browse files Browse the repository at this point in the history
Add support for perf_counter
  • Loading branch information
boxed committed Jun 18, 2021
2 parents 4685cb9 + 6a6bc99 commit bba5c0b
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 34 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Freezegun Changelog
===================

1.2.0
-----

* Add support for `time.perf_counter` (and `…_ns`)

1.1.0
-----

Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~
Expand Down
62 changes: 54 additions & 8 deletions freezegun/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,19 @@

_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)

real_time = time.time
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
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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),
]
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
56 changes: 35 additions & 21 deletions tests/test_datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -59,13 +60,15 @@ 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)
assert datetime.datetime.now().today() == datetime.datetime(2012, 1, 14)
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")
Expand Down Expand Up @@ -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()


Expand All @@ -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()


Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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():
Expand Down
23 changes: 19 additions & 4 deletions tests/test_ticking.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
import sys
import time

from unittest import mock
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from freezegun.api import FakeDate, FakeDatetime, _is_cpython

import pytest


def is_fake_date(obj):
return obj.__class__ is FakeDate
Expand All @@ -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):
Expand Down

0 comments on commit bba5c0b

Please sign in to comment.