-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Consider supporting a controlled clock (e.g., similar to async-solipsism) in TestClient #5834
Comments
@posita would you be able to post a complete example that I can run and use for investigation? The code you've posted has missing imports. |
Ah! Yes. Apologies. I corrected my original post, but see posita/aiohttp-solipsism for the test case in isolation. To run: git clone https://github.com/posita/aiohttp-solipsism.git
cd aiohttp-solipsism
# mkvirtualenv -a "$( pwd )" aiohttp-solipsism # or whatever
pip install --editable .
pytest -vv tests ☝️ That should work, but please let me know if it doesn't. |
I still need to read up about pytest-aiohttp (I've used aiohttp before but don't think I've used pytest-aiohttp), but if I delete all the references to async_solipsism from that file the test just hangs. I'd like to start from something that works without async-solipsism before trying to make async-solipsism work. |
Seems that aiohttp_client only works directly in the test, or in a fixture only when run using the aiohttp
If I load the aiohttp_client without using that loop, then Edit: as mentioned below, the |
Give the code below a try, together with the import async_solipsism
import pytest
from aiohttp import web
@pytest.fixture
def loop(mocker):
mocker.patch(
"aiohttp.test_utils.get_port_socket",
lambda host, port: async_solipsism.ListenSocket((host, port)),
)
loop = async_solipsism.EventLoop()
yield loop
loop.close()
async def test_integration(aiohttp_client):
app = web.Application()
client = await aiohttp_client(app, server_kwargs={"port": 80})
resp = await client.post("/hey", json={})
assert resp.status == 404 Some things to note:
|
See [this comment from aio-libs/aiohttp#5834](aio-libs/aiohttp#5834 (comment)). * Add bmerry/async-solipsism@fix-create-server-explicit-socket as submodule * Rename `event_loop` to `loop` * Add `pytest-mock` dependency * Patch `aiohttp.test_utils.get_port_socket` for test
See aio-libs/aiohttp#5834 (comment). * Add https://github.com/bmerry/async-solipsism/tree/fix-create-server-explicit-socket as submodule * Rename `event_loop` to `loop` * Add `pytest-mock` dependency * Patch `aiohttp.test_utils.get_port_socket` for test
See aio-libs/aiohttp#5834 (comment). * Add https://github.com/bmerry/async-solipsism/tree/fix-create-server-explicit-socket as submodule * Rename `event_loop` to `loop` * Add `pytest-mock` dependency * Patch `aiohttp.test_utils.get_port_socket` for test * `await` explicit client creation
Yup! Thanks, @bmerry! I made the above modifications (aiohttp-solipsism@168a544), and it looks like it works! FWIW, I am not generally a fan of the |
Maybe could my |
I did not know about aiotools before. But from a quick look it is just implementing a time-warping clock. My experience with that is that it doesn't work well with networking code (at least on Linux), because the kernel is not infinitely fast: after sending some data into a socketpair, it takes a little time to be available on the other end, and a time-warping clock causes read timeouts to trigger. I created async-solipsism to get around this problem by bypassing the kernel TCP/IP stack and use mock socketpairs which are instantaneous. |
Me neither, I just stick to unittest for my own projects. Is there anything else needed here, or should this be closed now? |
The example works by mocking an undocumented function inside Does the |
@Dreamsorcerer, I think with respect to the narrow question of compatibility with However, that led me to another, much broader question on what a best practice might be around something like this: import asyncio
import time
# …
async def test_foo():
start = time.time()
await asyncio.sleep(5.0) # <- has patched loop
end = time.time()
assert end - start >= 5.0 # <- will fail Why is the above important? Because many applications may introduce feature-specific delays and (understandably) want to lean on library interfaces to do that. Consider the following: import asyncio
import time
from typing import NoReturn
async def poll_state(state) -> NoReturn:
while True:
await asyncio.sleep(60.0)
now = time.time()
unlocked = state["unlock_time"] < now # <- problem
if unlocked:
state["done"] = True
async def test_poll_state() -> None:
now = time.time()
state = {"done": False, "unlock_time": now + 5.0} # 5 seconds from "now"
poller = asyncio.create_task(poll_state(state))
await asyncio.sleep(61.0) # <- uses mocked loop
# This will fail because of an advancement mismatch between loop.time() and time.time()
assert state["done"] == True ☝️ That's not the most testable thing without also mocking @achimnol, I haven't dug into Am I understanding correctly that a minimal application would entail the following? def test_foo():
vclock = aiotools.VirtualClock()
with vclock.patch_loop():
# do stuff When I get some more time, I'm happy to try refactoring posita/aiohttp-solipsism to use that and report back here (or maybe file a new issue in achimnol/aiotools if I get stuck). I don't know that we need to keep this bug open for that, though. |
I've put this into #5844 to help keep the specific issue clear.
Seems like a more general problem, my first inclination is that the library involved might consider patching I'll close this, but feel free to continue the discussion, and file a new issue if you figure that aiohttp should be providing something else in relation to |
I'm not a fan of that because:
In either case, broadly-applied library There are probably better solutions out there, but what would be nice is something like class TestLoop(…):
…
def wall_time(self) -> float:
if not hasattr(self, "_start"):
self._start = time.time() - self.time() # <- that conceptually belongs in a constructor somewhere
return self._start + time.time()
… More specifically, ☝️ If something like that were standard, One extreme of that trajectory is to re-implement all of The above is something akin to a more simplistic version of Twisted's |
That is poor practice even in the absence of a virtual clock. |
Thanks @bmerry! Agreed that Certainly, where one is in full control of time creation, a monotonic clock is the way to go. However, that's not always the case. Consider the case where one relies on a time value from an external source. (Bitcoin's Let's say I'm building an application that waits until my signed Bitcoin transactions are eligible to publish without being considered invalid by neighboring nodes. In practice, my deployment relies on Here's a simplistic implementation of the application code: import asyncio
import random
async def publish(publisher, transaction):
locked_until = transaction.nLockTime # seconds since epoch
await asyncio.sleep(...) # <- What goes in here such that I can test this function (e.g., with TestWallet and TestPublisher)?
sleep_time_sec = 1.0
while True:
try:
await publisher.publish(transaction)
except PublicationException:
await asyncio.sleep(sleep_time_sec * random.uniform(0.80, 1.20))
sleep_time_sec = min(2.0 * sleep_time_sec, 5 * 60.0) # top out at five minutes
else:
break
async def load_and_schedule(wallet, publisher):
transactions = wallet.load_unpublished_transactions()
tasks = []
for transaction in transactions:
tasks.append(asyncio.create_task(publish(publisher, transaction)))
return tasks Twisted standardizes this via a import twisted.…
import random
def publish(reactor, publisher, transaction):
locked_until = transaction.nLockTime # seconds since epoch
now = reactor.seconds() # reactor consolidates the concept of epoch time and loop time into this single interface
if locked_until + 2.0 < now:
# Probably not *quite* how one would do this in real life, but it's just to illustrate a point
reactor.callLater(locked_until + 2.0 - now, publish, reactor, publisher, transaction)
else:
# …
# … That reactor can be substituted at test time without affecting anything about application code, so long as application authors make sure to build time measurements around |
I guess the main thrust of my observation is that, for time-based application code, solely addressing |
* Make `async-solipsism` work in light of aio-libs/aiohttp#5834 * Add jitter to distribution (should have been in a separate commit)
* Make `async-solipsism` work in light of aio-libs/aiohttp#5834 * Add jitter to distribution (should have been in a separate commit)
See aio-libs/aiohttp#5834 (comment). * Add https://github.com/bmerry/async-solipsism/tree/fix-create-server-explicit-socket as submodule * Rename `event_loop` to `loop` * Add `pytest-mock` dependency * Patch `aiohttp.test_utils.get_port_socket` for test * `await` explicit client creation
@achimnol, I can verify that this is consistent with what I observe with my attempt in posita/aiohttp-solipsism@fd1a6f7. I may have misunderstood the intended use, but when I run my new test, I get:
Here's how you can run this yourself, if you like: $ git clone https://github.com/posita/aiohttp-solipsism.git
…
$ cd aiohttp-solipsism
$ mkvirtual env aiohttp-solipsism # or whatever you prefer
…
$ pip install --upgrade --requirement requirements.txt
…
$ pytest -vv tests/test_also_happy_together.py
…
E asyncio.exceptions.TimeoutError
… |
The API usage looks right, but my virtual clock has an assumption that the caller is using the standard asyncio event loop. |
It's unfortunate that—even as of this writing—
asyncio
apparently lacks decent abstractions to allow deterministic advancement of clocks in testing like Twisted has.I have unit tests that validate functionality that rely on both
aiohttp
andasyncio.sleep
.aiohttp
'sTestClient
allows me to testaiohttp
functionality, andasync-solipsism
affords a loop deterministic clock advancement without affecting wall time. However, I cannot use both at the same time, likely due to limitations inasync-solipsism
. Consider:This will result in the following error at runtime of tests:
Potential solutions
I would love a way to deterministically advance a clock in testing in a way compatible with
aiohttp
. That could mean refactoringTestClient
to work withinasync-solipsism
's limitations. It could mean expandingasync-solipsism
to allow use withTestClient
without modification to the latter. It could mean augmentingaiohttp
's internal testing loop to allow more control over the clock. It could mean something else entirely. I'm not sure.The text was updated successfully, but these errors were encountered: