-
Notifications
You must be signed in to change notification settings - Fork 160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ClockEventLoop class with fixture and test (close #95) #96
Changes from 12 commits
71701e1
3d34037
a723020
997afe2
6fbeb60
7d94f14
8841d15
98c6bf8
d757d19
ce6a2c7
1a8b887
f3efb64
8452d41
559e883
1d3cd51
ed8b05d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -189,10 +189,51 @@ def pytest_runtest_setup(item): | |
) | ||
|
||
|
||
class ClockEventLoop(asyncio.new_event_loop().__class__): | ||
""" | ||
A custom event loop that explicitly advances time when requested. Otherwise, | ||
this event loop advances time as expected. | ||
""" | ||
def __init__(self, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
self._offset = 0 | ||
|
||
def time(self): | ||
""" | ||
Return the time according the event loop's clock. | ||
|
||
This time is adjusted by the stored offset that allows for advancement | ||
with `advance_time`. | ||
""" | ||
return super().time() + self._offset | ||
|
||
async def advance_time(self, seconds): | ||
''' | ||
Advance time by a given offset in seconds. | ||
''' | ||
if seconds < 0: | ||
# cannot go backwards in time, so return immediately | ||
return | ||
|
||
# advance the clock by the given offset | ||
self._offset += seconds | ||
|
||
# ensure waiting callbacks are run before advancing the clock | ||
await asyncio.sleep(0, loop=self) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't it be before clock advance (line 219) then? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me take a closer look at these sleep calls. I recall as I was writing it that I had difficulty achieving a predictable number of loop iterations to get the test to react as expected. That's how I settled on sleeping 3 times. It is also the reason I ended up using |
||
|
||
if seconds > 0: | ||
# Once the clock is adjusted, new tasks may have just been scheduled for running | ||
# in the next pass through the event loop and advance again for the task | ||
# that calls `advance_time` | ||
await asyncio.sleep(0, loop=self) | ||
await asyncio.sleep(0, loop=self) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One should be enough. The point of having two was to have one before clock advance and one after it. |
||
|
||
|
||
# maps marker to the name of the event loop fixture that will be available | ||
# to marked test functions | ||
_markers_2_fixtures = { | ||
'asyncio': 'event_loop', | ||
'asyncio_clock': 'clock_event_loop', | ||
} | ||
|
||
|
||
|
@@ -204,6 +245,15 @@ def event_loop(request): | |
loop.close() | ||
|
||
|
||
@pytest.yield_fixture | ||
def clock_event_loop(request): | ||
"""Create an instance of the default event loop for each test case.""" | ||
loop = ClockEventLoop() | ||
asyncio.get_event_loop_policy().set_event_loop(loop) | ||
yield loop | ||
loop.close() | ||
|
||
|
||
def _unused_tcp_port(): | ||
"""Find an unused localhost TCP port from 1024-65535 and return it.""" | ||
with contextlib.closing(socket.socket()) as sock: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -86,3 +86,43 @@ async def test_asyncio_marker_method(self, event_loop): | |
def test_async_close_loop(event_loop): | ||
event_loop.close() | ||
return 'ok' | ||
|
||
|
||
@pytest.mark.asyncio_clock | ||
async def test_mark_asyncio_clock(): | ||
""" | ||
Test that coroutines marked with asyncio_clock are run with a ClockEventLoop | ||
""" | ||
import pytest_asyncio | ||
assert isinstance(asyncio.get_event_loop(), pytest_asyncio.plugin.ClockEventLoop) | ||
|
||
|
||
def test_clock_loop_loop_fixture(clock_event_loop): | ||
""" | ||
Test that the clock_event_loop fixture returns a proper instance of the loop | ||
""" | ||
import pytest_asyncio | ||
assert isinstance(asyncio.get_event_loop(), pytest_asyncio.plugin.ClockEventLoop) | ||
clock_event_loop.close() | ||
return 'ok' | ||
|
||
|
||
@pytest.mark.asyncio_clock | ||
async def test_clock_loop_advance_time(clock_event_loop): | ||
""" | ||
Test the sliding time event loop fixture | ||
""" | ||
async def short_nap(): | ||
await asyncio.sleep(1) | ||
|
||
# create the task | ||
task = clock_event_loop.create_task(short_nap()) | ||
assert not task.done() | ||
|
||
# start the task | ||
await clock_event_loop.advance_time(0) | ||
assert not task.done() | ||
|
||
# process the timeout | ||
await clock_event_loop.advance_time(1) | ||
assert task.done() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To make test reliable I tend to await explicitly: TIMEOUT=1 #define earlier in the file
await asyncio.wait_for(task, TIMEOUT) (and change short_nap/advance to 10) I do this because though advance_time marks task's sleep as finished, but the exact number of event loop iterations between this and the actual closing of the task is implementation-dependent. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The line picks up the look class at import time.
But the default loop factory can be changed in runtime by installing a custom policy for example.
I recall many projects that change the loop factory in
conftest.py
or top-level package's__init__.py
.For example, the PR cannot test clock-event-loop against proactor event loop on Windows even if the policy was explicitly changed.
Honestly, I don't know how to solve the problem in unambiguous way but want to point on the problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand the problem here, but the solution is not immediately obvious. Let me take a crack at seeing if I can make a function that produces a class to solve this.