From 9120ba310f1050e5d0d0ededc6b91e82f8b33791 Mon Sep 17 00:00:00 2001 From: "Derek A. Thomas" Date: Mon, 18 Feb 2019 12:55:49 -0800 Subject: [PATCH 1/7] foo --- pytest_asyncio/plugin.py | 25 +++++++++++++++++++++++++ tests/test_simple_35.py | 21 +++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 1cf4b0b7..839a32b3 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -166,6 +166,26 @@ def pytest_runtest_setup(item): } +class EventLoopClockAdvancer: + __slots__ = ("offset", "_base_time",) + def __init__(self, loop): + self.offset = 0.0 + self._base_time = loop.time + loop.time = self.time + + def time(self): + return self._base_time() + self.offset + + def __call__(self, seconds): + if seconds > 0: + # advance the clock by the given offset + self.offset += seconds + + # Once the clock is adjusted, new tasks may have just been + # scheduled for running in the next pass through the event loop + return self.create_task(asyncio.sleep(0)) + + @pytest.yield_fixture def event_loop(request): """Create an instance of the default event loop for each test case.""" @@ -198,3 +218,8 @@ def factory(): return port return factory + + +@pytest.fixture +def advance_time(event_loop): + return EventLoopClockAdvancer(event_loop) diff --git a/tests/test_simple_35.py b/tests/test_simple_35.py index 1e4d697c..59781a89 100644 --- a/tests/test_simple_35.py +++ b/tests/test_simple_35.py @@ -86,3 +86,24 @@ async def test_asyncio_marker_method(self, event_loop): def test_async_close_loop(event_loop): event_loop.close() return 'ok' + + +@pytest.mark.asyncio +async def test_advance_time_fixture(advance_time): + """ + Test the `advance_time` fixture + """ + # A task is created that will sleep some number of seconds + SLEEP_TIME = 10 + + # create the task + task = event_loop.create_task(asyncio.sleep(SLEEP_TIME)) + assert not task.done() + + # start the task + await advance_time(0) + assert not task.done() + + # process the timeout + await advance_time(SLEEP_TIME) + assert task.done() From ddc08ba529dde33aab68c5ed81edd2a345576dc7 Mon Sep 17 00:00:00 2001 From: "Derek A. Thomas" Date: Mon, 18 Feb 2019 13:10:04 -0800 Subject: [PATCH 2/7] add advance_time fixture and test (closes #83, #95, #96 #110) --- pytest_asyncio/plugin.py | 43 ++++++++++++++++++++++++++++++++++++++++ tests/test_simple_35.py | 21 ++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 9f4e10d7..8ba7fd01 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -201,6 +201,44 @@ def pytest_runtest_setup(item): } +class EventLoopClockAdvancer: + """ + A helper object that when called will advance the event loop's time. If the + call is awaited, the caller task will wait an iteration for the update to + wake up any awaiting handlers. + """ + __slots__ = ("offset", "loop", "_base_time",) + + def __init__(self, loop): + self.offset = 0.0 + self._base_time = loop.time + self.loop = loop + + # incorporate offset timing into the event loop + self.loop.time = self.time + + def time(self): + """ + Return the time according to the event loop's clock. The time is + adjusted by an offset. + """ + return self._base_time() + self.offset + + def __call__(self, seconds): + """ + Advance time by a given offset in seconds. Returns an awaitable + that will complete after all tasks scheduled for after advancement + of time are proceeding. + """ + if seconds > 0: + # advance the clock by the given offset + self.offset += abs(seconds) + + # Once the clock is adjusted, new tasks may have just been + # scheduled for running in the next pass through the event loop + return self.loop.create_task(asyncio.sleep(0)) + + @pytest.yield_fixture def event_loop(request): """Create an instance of the default event loop for each test case.""" @@ -237,3 +275,8 @@ def factory(): return port return factory + + +@pytest.fixture +def advance_time(event_loop): + return EventLoopClockAdvancer(event_loop) diff --git a/tests/test_simple_35.py b/tests/test_simple_35.py index 1e4d697c..c3106967 100644 --- a/tests/test_simple_35.py +++ b/tests/test_simple_35.py @@ -86,3 +86,24 @@ async def test_asyncio_marker_method(self, event_loop): def test_async_close_loop(event_loop): event_loop.close() return 'ok' + + +@pytest.mark.asyncio +async def test_advance_time_fixture(event_loop, advance_time): + """ + Test the `advance_time` fixture + """ + # A task is created that will sleep some number of seconds + SLEEP_TIME = 10 + + # create the task + task = event_loop.create_task(asyncio.sleep(SLEEP_TIME)) + assert not task.done() + + # start the task + await advance_time(0) + assert not task.done() + + # process the timeout + await advance_time(SLEEP_TIME) + assert task.done() From 6ddbbe3fda6dc1100df832ffb44c9bfb4380ffa0 Mon Sep 17 00:00:00 2001 From: "Derek A. Thomas" Date: Tue, 14 May 2019 10:28:32 -0700 Subject: [PATCH 3/7] simplify EventLoopClockAdvancer and clarify __call__ method --- pytest_asyncio/plugin.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 839a32b3..9056adcc 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -168,6 +168,7 @@ def pytest_runtest_setup(item): class EventLoopClockAdvancer: __slots__ = ("offset", "_base_time",) + def __init__(self, loop): self.offset = 0.0 self._base_time = loop.time @@ -176,14 +177,17 @@ def __init__(self, loop): def time(self): return self._base_time() + self.offset - def __call__(self, seconds): + async def __call__(self, seconds): + # sleep so that the loop does everything currently waiting + await asyncio.sleep(0) + if seconds > 0: # advance the clock by the given offset self.offset += seconds - # Once the clock is adjusted, new tasks may have just been - # scheduled for running in the next pass through the event loop - return self.create_task(asyncio.sleep(0)) + # Once the clock is adjusted, new tasks may have just been + # scheduled for running in the next pass through the event loop + await asyncio.sleep(0) @pytest.yield_fixture From 566af4842dc69024024017c9b3c77d65c4fba8eb Mon Sep 17 00:00:00 2001 From: "Derek A. Thomas" Date: Tue, 14 May 2019 10:29:39 -0700 Subject: [PATCH 4/7] add more tests to verify the advance_time fixture operations --- tests/test_simple_35.py | 55 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/tests/test_simple_35.py b/tests/test_simple_35.py index 59781a89..00ea9aca 100644 --- a/tests/test_simple_35.py +++ b/tests/test_simple_35.py @@ -89,9 +89,9 @@ def test_async_close_loop(event_loop): @pytest.mark.asyncio -async def test_advance_time_fixture(advance_time): +async def test_advance_time_fixture(event_loop, advance_time): """ - Test the `advance_time` fixture + Test the `advance_time` fixture using a sleep timer """ # A task is created that will sleep some number of seconds SLEEP_TIME = 10 @@ -107,3 +107,54 @@ async def test_advance_time_fixture(advance_time): # process the timeout await advance_time(SLEEP_TIME) assert task.done() + + +@pytest.mark.asyncio +async def test_advance_time_fixture_call_later(event_loop, advance_time): + """ + Test the `advance_time` fixture using loop.call_later + """ + # A task is created that will sleep some number of seconds + SLEEP_TIME = 10 + result = [] + + # create a simple callback that adds a value to result + def callback(): + result.append(True) + + # create the task + event_loop.call_later(SLEEP_TIME, callback) + + # start the task + await advance_time(0) + assert not result + + # process the timeout + await advance_time(SLEEP_TIME) + assert result + + +@pytest.mark.asyncio +async def test_advance_time_fixture_coroutine(event_loop, advance_time): + """ + Test the `advance_time` fixture using loop.call_later + """ + # A task is created that will sleep some number of seconds + SLEEP_TIME = 10 + result = [] + + # create a simple callback that adds a value to result + async def callback(): + await asyncio.sleep(SLEEP_TIME) + result.append(True) + + # create the task + task = event_loop.create_task(callback()) + + # start the task + await advance_time(0) + assert not task.done() + + # process the timeout + await advance_time(SLEEP_TIME) + assert task.done() and result From 725a267047bd80f746d93e5deee0d86f560e823b Mon Sep 17 00:00:00 2001 From: "Derek A. Thomas" Date: Tue, 14 May 2019 10:40:11 -0700 Subject: [PATCH 5/7] fix bad merge --- pytest_asyncio/plugin.py | 37 ++++++++----------------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index b62c599c..d8f87fae 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -159,30 +159,6 @@ def pytest_runtest_setup(item): ) -class EventLoopClockAdvancer: - __slots__ = ("offset", "_base_time",) - - def __init__(self, loop): - self.offset = 0.0 - self._base_time = loop.time - loop.time = self.time - - def time(self): - return self._base_time() + self.offset - - async def __call__(self, seconds): - # sleep so that the loop does everything currently waiting - await asyncio.sleep(0) - - if seconds > 0: - # advance the clock by the given offset - self.offset += seconds - - # Once the clock is adjusted, new tasks may have just been - # scheduled for running in the next pass through the event loop - await asyncio.sleep(0) - - class EventLoopClockAdvancer: """ A helper object that when called will advance the event loop's time. If the @@ -206,19 +182,22 @@ def time(self): """ return self._base_time() + self.offset - def __call__(self, seconds): + async def __call__(self, seconds): """ Advance time by a given offset in seconds. Returns an awaitable that will complete after all tasks scheduled for after advancement of time are proceeding. """ + # sleep so that the loop does everything currently waiting + await asyncio.sleep(0) + if seconds > 0: # advance the clock by the given offset - self.offset += abs(seconds) + self.offset += seconds - # Once the clock is adjusted, new tasks may have just been - # scheduled for running in the next pass through the event loop - return self.loop.create_task(asyncio.sleep(0)) + # Once the clock is adjusted, new tasks may have just been + # scheduled for running in the next pass through the event loop + await asyncio.sleep(0) @pytest.yield_fixture From 9c17c458bb461ed6ef496945d25182aef5a1ef35 Mon Sep 17 00:00:00 2001 From: "Derek A. Thomas" Date: Thu, 23 May 2019 15:27:37 -0700 Subject: [PATCH 6/7] add pytest-option '--advance-time-sleep' for configuration --- pytest_asyncio/plugin.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index d8f87fae..f8152d01 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -32,6 +32,13 @@ def pytest_configure(config): "run using an asyncio event loop") +def pytest_addoption(parser): + """inject commandline option for advance_time""" + parser.addoption( + "--advance-time-sleep", type=float, default=0, help="sleep duration for advance_time fixture", + ) + + @pytest.mark.tryfirst def pytest_pycollect_makeitem(collector, name, obj): """A pytest hook to collect asyncio coroutines.""" @@ -165,12 +172,15 @@ class EventLoopClockAdvancer: call is awaited, the caller task will wait an iteration for the update to wake up any awaiting handlers. """ - __slots__ = ("offset", "loop", "_base_time",) - def __init__(self, loop): + __slots__ = ("offset", "loop", "sleep_duration", "_base_time") + + def __init__(self, loop, sleep_duration=0.0): + breakpoint() self.offset = 0.0 self._base_time = loop.time self.loop = loop + self.sleep_duration = sleep_duration # incorporate offset timing into the event loop self.loop.time = self.time @@ -189,7 +199,7 @@ async def __call__(self, seconds): of time are proceeding. """ # sleep so that the loop does everything currently waiting - await asyncio.sleep(0) + await asyncio.sleep(self.sleep_duration) if seconds > 0: # advance the clock by the given offset @@ -197,7 +207,7 @@ async def __call__(self, seconds): # Once the clock is adjusted, new tasks may have just been # scheduled for running in the next pass through the event loop - await asyncio.sleep(0) + await asyncio.sleep(self.sleep_duration) @pytest.yield_fixture @@ -239,5 +249,6 @@ def factory(): @pytest.fixture -def advance_time(event_loop): - return EventLoopClockAdvancer(event_loop) +def advance_time(event_loop, request): + sleep_duration = request.config.getoption("--advance-time-sleep") + return EventLoopClockAdvancer(event_loop, sleep_duration) From 203b09360ae6543496895541ef5e1f118283751d Mon Sep 17 00:00:00 2001 From: "Derek A. Thomas" Date: Thu, 23 May 2019 15:40:18 -0700 Subject: [PATCH 7/7] fix pytest and remove --advance-time-sleep option - for more simplicity the advnace-time-sleep has simply been changed to 1e-6 so that it works on most systems. - remove debug code --- pytest_asyncio/plugin.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index f8152d01..37c63b24 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -32,13 +32,6 @@ def pytest_configure(config): "run using an asyncio event loop") -def pytest_addoption(parser): - """inject commandline option for advance_time""" - parser.addoption( - "--advance-time-sleep", type=float, default=0, help="sleep duration for advance_time fixture", - ) - - @pytest.mark.tryfirst def pytest_pycollect_makeitem(collector, name, obj): """A pytest hook to collect asyncio coroutines.""" @@ -175,8 +168,7 @@ class EventLoopClockAdvancer: __slots__ = ("offset", "loop", "sleep_duration", "_base_time") - def __init__(self, loop, sleep_duration=0.0): - breakpoint() + def __init__(self, loop, sleep_duration=1e-6): self.offset = 0.0 self._base_time = loop.time self.loop = loop @@ -250,5 +242,4 @@ def factory(): @pytest.fixture def advance_time(event_loop, request): - sleep_duration = request.config.getoption("--advance-time-sleep") - return EventLoopClockAdvancer(event_loop, sleep_duration) + return EventLoopClockAdvancer(event_loop)