Skip to content

Commit

Permalink
Merge pull request #470 from marcinsulikowski/improve-asyncio-support
Browse files Browse the repository at this point in the history
Improve asyncio support to avoid hangs of asyncio.sleep()
  • Loading branch information
boxed authored Feb 22, 2023
2 parents 28d1a36 + 89d36e2 commit 4f44963
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 8 deletions.
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ Patches and Suggestions
- `Lukasz Balcerzak <https://github.com/lukaszb>`_
- `Hannes Ljungberg <hannes@5monkeys.se>`_
- `staticdev <staticdev-support@proton.me>`_
- `Marcin Sulikowski <https://github.com/marcinsulikowski>`_
5 changes: 5 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Freezegun Changelog
===================

1.3.0
-----

* Fixed `asyncio` support to avoid `await asyncio.sleep(1)` hanging forever.

1.2.2
-----

Expand Down
20 changes: 18 additions & 2 deletions freezegun/api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from . import config
from ._async import wrap_coroutine
import asyncio
import copyreg
import dateutil
import datetime
Expand Down Expand Up @@ -726,6 +727,21 @@ def start(self):
setattr(module, attribute_name, fake)
add_change((module, attribute_name, attribute_value))

# To avoid breaking `asyncio.sleep()`, let asyncio event loops see real
# monotonic time even though we've just frozen `time.monotonic()` which
# is normally used there. If we didn't do this, `await asyncio.sleep()`
# would be hanging forever breaking many tests that use `freeze_time`.
#
# Note that we cannot statically tell the class of asyncio event loops
# because it is not officially documented and can actually be changed
# at run time using `asyncio.set_event_loop_policy`. That's why we check
# the type by creating a loop here and destroying it immediately.
event_loop = asyncio.new_event_loop()
event_loop.close()
EventLoopClass = type(event_loop)
add_change((EventLoopClass, "time", EventLoopClass.time))
EventLoopClass.time = lambda self: real_monotonic()

return freeze_factory

def stop(self):
Expand All @@ -739,8 +755,8 @@ def stop(self):
datetime.date = real_date
copyreg.dispatch_table.pop(real_datetime)
copyreg.dispatch_table.pop(real_date)
for module, module_attribute, original_value in self.undo_changes:
setattr(module, module_attribute, original_value)
for module_or_object, attribute, original_value in self.undo_changes:
setattr(module_or_object, attribute, original_value)
self.undo_changes = []

# Restore modules loaded after start()
Expand Down
75 changes: 69 additions & 6 deletions tests/test_asyncio.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,80 @@
import asyncio
import datetime
from textwrap import dedent
from unittest import SkipTest
import time

from freezegun import freeze_time


def test_time_freeze_coroutine():
if not asyncio:
raise SkipTest('asyncio required')

def test_datetime_in_coroutine():
@freeze_time('1970-01-01')
async def frozen_coroutine():
assert datetime.date.today() == datetime.date(1970, 1, 1)

asyncio.run(frozen_coroutine())


def test_freezing_time_in_coroutine():
"""Test calling freeze_time while executing asyncio loop."""
async def coroutine():
with freeze_time('1970-01-02'):
assert time.time() == 86400
with freeze_time('1970-01-03'):
assert time.time() == 86400 * 2

asyncio.run(coroutine())


def test_freezing_time_before_running_coroutine():
"""Test calling freeze_time before executing asyncio loop."""
async def coroutine():
assert time.time() == 86400
with freeze_time('1970-01-02'):
asyncio.run(coroutine())


def test_asyncio_sleeping_not_affected_by_freeze_time():
"""Test that asyncio.sleep() is not affected by `freeze_time`.
This test ensures that despite freezing time using `freeze_time`,
the asyncio event loop can see real monotonic time, which is required
to make things like `asyncio.sleep()` work.
"""

async def coroutine():
# Sleeping with time frozen should sleep the expected duration.
before_sleep = time.time()
with freeze_time('1970-01-02'):
await asyncio.sleep(0.05)
assert 0.02 <= time.time() - before_sleep < 0.3

# Exiting `freeze_time` the time should not break asyncio sleeping.
before_sleep = time.time()
await asyncio.sleep(0.05)
assert 0.02 <= time.time() - before_sleep < 0.3

asyncio.run(coroutine())


def test_asyncio_to_call_later_with_frozen_time():
"""Test that asyncio `loop.call_later` works with frozen time."""
# `to_call_later` will be called by asyncio event loop and should add
# the Unix timestamp of 1970-01-02 00:00 to the `timestamps` list.
timestamps = []
def to_call_later():
timestamps.append(time.time())

async def coroutine():
# Schedule calling `to_call_later` in 100 ms.
asyncio.get_running_loop().call_later(0.1, to_call_later)

# Sleeping for 10 ms should not result in calling `to_call_later`.
await asyncio.sleep(0.01)
assert timestamps == []

# But sleeping more (150 ms in this case) should call `to_call_later`
# and we should see `timestamps` updated.
await asyncio.sleep(0.15)
assert timestamps == [86400]

with freeze_time('1970-01-02'):
asyncio.run(coroutine())

0 comments on commit 4f44963

Please sign in to comment.