pytest RuntimeError: Event loop is closed FastApi - pytest

I receive an error RuntimeError: Event loop is closed each time when i try to make more than one async call function inside my test. I already tried to use all suggestions on stackoverflow to rewrite event_loop fixture but nothing works. I wonder what i'm missing
Run test command: python -m pytest tests/ --asyncio-mode=auto
requirements.txt
pytest==7.1.2
pytest-asyncio==0.18.3
pytest-html==3.1.1
pytest-metadata==2.0.1
test.py
async def test_user(test_client_fast_api):
assert 200 == 200
request_first = test_client_fast_api.post( # works fine
"/first_route",
)
request_second = test_client_fast_api.post( # recieve RuntimeError: Event loop is closed
"/second_route",
)
conftest.py
#pytest.fixture()
def event_loop():
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
yield loop
loop.close()

It took me all afternoon to solve this problem.
I also try to succeed from other people's code, here is my code.
Add a file conftest.py to the directory where the test script is placed.
And write the following code.
import pytest
from main import app
from httpx import AsyncClient
#pytest.fixture(scope="session")
def anyio_backend():
return "asyncio"
#pytest.fixture(scope="session")
async def client():
async with AsyncClient(app=app, base_url="http://test") as client:
print("Client is ready")
yield client
And then write a test script test_xxx.py.
import pytest
from httpx import AsyncClient
#pytest.mark.anyio
async def test_run_not_exists_schedule(client: AsyncClient):
response = await client.get("/schedule/list")
assert response.status_code == 200
schedules = response.json()["data"]["schedules"]
schedules_exists = [i["id"] for i in schedules]
not_exists_id = max(schedules_exists) + 1
request_body = {"id": not_exists_id}
response = await client.put("/schedule/run_cycle", data=request_body)
assert response.status_code != 200
#pytest.mark.anyio
async def test_run_adfasdfw(client: AsyncClient):
response = await client.get("/schedule/list")
assert response.status_code == 200
schedules = response.json()["data"]["schedules"]
schedules_exists = [i["id"] for i in schedules]
not_exists_id = max(schedules_exists) + 1
request_body = {"id": not_exists_id}
response = await client.put("/schedule/run_cycle", data=request_body)
assert response.status_code != 200
This is the real test code for my own project. You can change it to your own.Finally, run in the project's terminal python -m pytest.If all goes well, it should be ok's.This may involve libraries that need to be installed.
pytest
httpx

Yeah wow I had a similar afternoon to your experience #Bai Jinge
This is the event loop fixture and TestClient pattern that worked for me:
from asyncio import get_event_loop
from unittest import TestCase
from async_asgi_testclient import TestClient
#pytest.fixture(scope="module")
def event_loop():
loop = get_event_loop()
yield loop
#pytest.mark.asyncio
async def test_example_test_case(self):
async with TestClient(app) as async_client:
response = await async_client.get(
"/api/v1/example",
query_string={"example": "param"},
)
assert response.status_code == HTTP_200_OK
Ref to relevant GitHub issue: https://github.com/tiangolo/fastapi/issues/2006#issuecomment-689611040
Please note - I could NOT figure our how to use Class based tests. Neither unittest.TestCase or asynctest.case.TestCase would work for me. pytest-asyncio docs (here) state that:
Test classes subclassing the standard unittest library are not supported, users are recommended to use unittest.IsolatedAsyncioTestCase or an async framework such as asynctest.

Related

SqlAlchemy select hangs in FastAPI test setup

I developed a FastAPI app with WebSockets and I'm trying to test it. I need async tests in order to check data in the database during tests. My setup looks like this: I have a fixture that generates a database session:
#pytest_asyncio.fixture(scope="session")
async def async_engine() -> AsyncEngine:
yield create_async_engine(settings.TEST_DATABASE_URL)
#pytest_asyncio.fixture(scope="function")
async def session(async_engine):
AsyncSessionLocal = sessionmaker(
bind=async_engine, autoflush=False, expire_on_commit=False, future=True, class_=AsyncSession
)
yield AsyncSessionLocal()
Then I have a fixture to create a client:
#pytest_asyncio.fixture(scope="function")
async def client(session):
app.dependency_overrides[get_db_session] = lambda: session
async with AsyncClient(transport=ASGIWebSocketTransport(app), base_url="http://test") as client:
yield client
The transport=ASGIWebSocketTransport(app) part is from httpx-ws library, httpx itself does not support websockets.
The tests looks like this:
#pytest.mark.asyncio
async def test_http(session, client):
r = await client.get('/test/')
#pytest.mark.asyncio
async def test_ws(session, client):
async with aconnect_ws("/ws/", client) as ws:
await ws.send_json({})
test_http works as expected. However, test_ws does not: all sql queries in /ws/ endpoint hang forever. When engine is created with echo=True I see my sql statement in logs like sqlalchemy.engine.Engine SELECT test.id FROM test and then nothing. I've tried looking into pg_stat_activity, pg_locks and pg_blocking_pids(pid) with no success.
At this point I'm stuck: I've read through httpx-ws code and couldn't find any potential problems. What else can I do to figure out why database connection hangs in my case?

Pytest asyncio - run multiple tests gives error regarding event loop

I am using pytest with pytest-asyncio to run async tests. What is strange is that these tests only work if there is exactly one of them. For example:
#pytest.mark.asyncio
async def test_foo():
client = await get_db_connection()
user = await client.DO_QUERY
response = await FunctionUnderTest(
db=client, ...args
)
assert response.id is not None
#pytest.mark.asyncio
async def test_error_foo():
client = await get_db_connection()
with pytest.raises(MissingRequiredError):
await FunctionUnderTest(
db=client, ...args
)
If I comment out either of those tests, the remaining one will pass, but running both together gives:
RuntimeError: Task <Task pending name='Task-5' coro=<test_error_foo() running at /tests/test_function_under_test.py:44> cb=[_run_until_complete_cb() at /usr/lib/python3.10/asyncio/base_events.py:184]> got Future <Future pending> attached to a different loop
I would have expected pytest-asyncio to create a single event loop and run all the tests sequentially, but this does not seem to work.

How to create a websocket manager in Solana-py

I am using Python v3.7 + solana v0.21.0 and trying to create a websocket manager handling several different subscriptions, but the code seems to block whenever websocket.recv()
or asyncstdlib.enumerate(websocket) is called. Examples from the docs:
import asyncio
from asyncstdlib import enumerate
from solana.rpc.websocket_api import connect
async def main():
# First example using websocket.recv()
async with connect("wss://api.devnet.solana.com") as websocket:
await websocket.logs_subscribe()
first_resp = await websocket.recv()
subscription_id = first_resp.result
next_resp = await websocket.recv() <--- Blocks until msg received.
print(next_resp)
await websocket.logs_unsubscribe(subscription_id)
# Second example using client as an infinite asynchronous iterator:
async with connect("wss://api.devnet.solana.com") as websocket:
await websocket.logs_subscribe()
first_resp = await websocket.recv()
subscription_id = first_resp.result
async for idx, msg in enumerate(websocket): <--- Blocks until msg received.
if idx == 3:
break
print(msg)
await websocket.logs_unsubscribe(subscription_id)
asyncio.run(main())
The idea is being able to iterate an infinite loop so new subscriptions can be added to the websocket for instance:
from solana.rpc.request_builder import LogsSubscribeFilter
from solana.rpc.websocket_api import connect
from asgiref.sync import sync_to_async
from solana.publickey import PublicKey
from time import sleep
async def websocket_manager(rpc: str):
async with connect(rpc) as websocket:
while True:
active_pubkeys = await sync_to_async(get_my_active_pubkeys)()
if active_pubkeys:
# Add missing pubkeys
for pubkey in active_pubkeys:
if ws.get(pubkey) in websocket.subscriptions:
continue
print(f"Subscribe to {pubkey}")
await websocket.logs_subscribe(LogsSubscribeFilter.mentions(PublicKey(pubkey)))
first_resp = await websocket.recv()
ws[pubkey] = first_resp.result # Maps the pubkey to the subscription ID
# Delete non used subscriptions:
for non_used_pubkey in set(active_pubkeys) ^ set(ws.keys()):
if non_used_pubkey in ws:
print(f"Delete subscription for pubkey #{non_used_pubkey}")
websocket.account_unsubscribe(ws[non_used_pubkey])
ws.pop(non_used_pubkey)
# <-- HERE HOW TO ITERATE SUBSCRIPTIONS WITHOUT BLOCKING THE MANAGER????
sleep(30) # Sleep for 30 seconds
Would be it be safe using a new thread or a subprocess for reading the websocket messages so they don't block the main function?
I figured it out, just needed to use task = asyncio.create_task(my_msg_listener_function(websocket, ws)) to have another task running concurrently and taking care of the websocket messages.

Pytest asyncio validate working asyncio.wait( , return_when )

Within a class I have a have a asyncio loop with is created with run_until_complete and the argument return_when. The functions within tasks are running as long as the application is running
I would like to to create a test with Pytest that validate the following situation:
task asyncfoobar or asynctesting is finished for some reason
the loop should stop running and the finally statement will be called.
When testing the application for this situation, it works as expected.
A test is preferred to easily validate it keeps working correct.
Pytest is used for this. How can this be done?
Catching the log line at the finally statement?
Snippets of the code that need to be tested:
def asyncfoobar:
try:
....
finally:
return
def asynctesting(a,b):
while True:
....
await asyncio.sleep(10)
class Do_something:
def start
try:
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.tasks=[
self.loop.create_task(asyncfoobar(config)),
asyncio.get_event_loop().create_task(asynctesting(a, b), )
]
self.loop.run_until_complete(
asyncio.wait(self.tasks,
return_when=asyncio.FIRST_COMPLETED)
)
finally:
logging.info("We are stopping")
return

aiohttp - How to save a persistent session in class namespace

I am trying to use aiohttp in one of my projects and struggling to figure out how to create a persistent aiohttp.ClientSession object. I have gone through the official aiohttp documentation but did not find it help in this context.
I have looked through other online forums and noticed that a lot has changed ever since aiohttp was created. In some examples on github, the aiohttp author is shown to be creating a ClientSession outside a coroutine functions (i.e. class Session: def __init__(self): self.session = aiohttp.ClientSession()). I also found that one should not create a ClientSession outside coroutine.
I have tried the following:
class Session:
def __init__(self):
self._session = None
async def create_session(self):
self._session = aiohttp.ClientSession()
async fetch(self, url):
if self._session is None:
await self.create_session()
async with self._session.get(url) as resp:
return await resp.text()
I am getting a lot of warning about UnclosedSession and connector. I also frequently get SSLError. I also noticed that 2 out of three calls gets hung and I have to CTRL+C to kill it.
With requests I can simply initialize the session object in __init__, but it's not as simple as this with aiohttp.
I do not see any issues if I use the following (which is what I see as example all over the place) but unfortunately here I end up creating ClientSession with every request.
def fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return await resp.text()
I can wrap aiohttp.ClientSession() in another function and use that as context-manager, but then too I would end up creating a new session object every time I call the wrapper function. I am trying to figure how to save a aiohttp.ClientSession in class namespace and reuse it.
Any help would be greatly appreciated.
Here is working example:
from aiohttp import ClientSession, TCPConnector
import asyncio
class CS:
_cs: ClientSession
def __init__(self):
self._cs = ClientSession(connector=TCPConnector(verify_ssl=False))
async def get(self, url):
async with self._cs.get(url) as resp:
return await resp.text()
async def close(self):
await self._cs.close()
async def func():
cs = CS()
print(await cs.get('https://google.com'))
await cs.close() # you must close session
loop = asyncio.get_event_loop()
loop.run_until_complete(func())
You can do it.
I implemented a way to share session when writing Django programs (using asgi).Use pid to mark the session of different processes, which is convenient for django to call between different processes.
After actual testing, I can directly call the shared session.
Django 3.2
uvicorn
aiohttp.py
import os
import asyncio
import aiohttp
import logging
session_list = {}
logger = logging.getLogger(__name__)
class Req:
#property
def set_session(self):
try:
loop = asyncio.get_running_loop()
except:
loop = asyncio.get_event_loop()
asyncio.set_event_loop(loop)
session = aiohttp.ClientSession(loop=loop)
session_list.update({os.getpid(): session})
return session
def __init__(self):
if session_list.get(os.getpid()):
self.session = session_list.get(os.getpid())
else:
self.session = self.set_session
async def test(self):
if session_list:
session = session_list.get(os.getpid())
if session and session.closed:
session_list.pop(os.getpid())
session = self.set_session
else:
session = self.set_session
if not session or session.loop.is_running():
session = self.set_session
logger.warning("session abnormal")
result = await session.get("http://httpbing.org/get")
print(result.status)
req = Req()
views.py
from django.http import HttpResponse
from django.shortcuts import render # noqa
from django.views.generic import View
from django.utils.decorators import classonlymethod
import asyncio
class TTT(View):
#classonlymethod
def as_view(cls, **initkwargs):
view = super().as_view(**initkwargs)
view._is_coroutine = asyncio.coroutines._is_coroutine
return view
async def get(self, request):
await req.test()
return HttpResponse("ok")