1+ import asyncio
12import importlib
23import os
3- from typing import cast
4+ import threading
5+ import warnings
6+ from collections .abc import Generator
7+ from types import SimpleNamespace
48
59import pytest
610from aiohttp import BasicAuth , ClientSession , web
711from yarl import URL
812
13+ from aidbox_python_sdk .aidboxpy import AsyncAidboxClient
14+
915from . import app_keys as ak
1016
17+ _TEST_SERVER_URL = "http://127.0.0.1:8081"
18+
1119
1220def pytest_addoption (parser ):
1321 parser .addini (
@@ -34,20 +42,92 @@ def create_app(request):
3442 return _load_create_app (path )
3543
3644
37- async def start_app (aiohttp_client , create_app ):
38- app = await aiohttp_client (create_app (), server_kwargs = {"host" : "0.0.0.0" , "port" : 8081 })
39- sdk = cast (web .Application , app .server .app )[ak .sdk ]
45+ @pytest .fixture (scope = "session" , autouse = True )
46+ def app (create_app ) -> Generator [web .Application , None , None ]:
47+ """Start the aiohttp application server in a background thread.
48+
49+ Uses a dedicated event loop running continuously via loop.run_forever()
50+ in a daemon thread. This is necessary (rather than an async fixture) because
51+ the app needs the loop running at all times to handle external callbacks
52+ from Aidbox (subscriptions, SDK heartbeats, etc.), not just during await
53+ points in test code.
54+
55+ The server is guaranteed to be listening before any test runs (no sleep-based
56+ waiting) since site.start() completes synchronously before yielding.
57+ """
58+
59+ app = create_app ()
60+ loop = asyncio .new_event_loop ()
61+
62+ runner = web .AppRunner (app )
63+ loop .run_until_complete (runner .setup ())
64+ site = web .TCPSite (runner , host = "0.0.0.0" , port = 8081 )
65+ loop .run_until_complete (site .start ())
66+
67+ thread = threading .Thread (target = loop .run_forever , daemon = True )
68+ thread .start ()
69+
70+ yield app
71+
72+ loop .call_soon_threadsafe (loop .stop )
73+ thread .join (timeout = 5 )
74+ loop .run_until_complete (runner .cleanup ())
75+ loop .close ()
76+
77+
78+ @pytest .fixture
79+ async def client (app ):
80+ server = SimpleNamespace (app = app )
81+ session = ClientSession (base_url = URL (_TEST_SERVER_URL ))
82+ wrapper = SimpleNamespace (server = server )
83+ wrapper .get = session .get
84+ wrapper .post = session .post
85+ wrapper .put = session .put
86+ wrapper .patch = session .patch
87+ wrapper .delete = session .delete
88+ wrapper .request = session .request
89+ wrapper ._session = session
90+ try :
91+ yield wrapper
92+ finally :
93+ await session .close ()
94+
95+
96+ @pytest .fixture
97+ async def safe_db (aidbox_client : AsyncAidboxClient , sdk ):
98+ results = await aidbox_client .execute (
99+ "/$psql" ,
100+ data = {"query" : "SELECT last_value from transaction_id_seq;" },
101+ )
102+ txid = results [0 ]["result" ][0 ]["last_value" ]
103+ sdk ._test_start_txid = int (txid )
104+
105+ yield txid
106+
40107 sdk ._test_start_txid = - 1
108+ await aidbox_client .execute (
109+ "/$psql" ,
110+ data = {"query" : f"select drop_before_all({ txid } );" },
111+ params = {"execute" : "true" },
112+ )
113+
114+
115+ @pytest .fixture
116+ def sdk (app ):
117+ return app [ak .sdk ]
41118
42- return app
43119
120+ @pytest .fixture
121+ def aidbox_client (app ):
122+ return app [ak .client ]
44123
45- @pytest .fixture ()
46- async def client (aiohttp_client , create_app ):
47- """Instance of app's server and client"""
48- return await start_app (aiohttp_client , create_app )
49124
125+ @pytest .fixture
126+ def aidbox_db (app ):
127+ return app [ak .db ]
50128
129+
130+ # Deprecated
51131class AidboxSession (ClientSession ):
52132 def __init__ (self , * args , base_url = None , ** kwargs ):
53133 base_url_resolved = base_url or os .environ .get ("AIDBOX_BASE_URL" )
@@ -63,51 +143,17 @@ async def _request(self, method, path, *args, **kwargs):
63143 return await super ()._request (method , url , * args , ** kwargs )
64144
65145
66- @pytest .fixture ()
67- async def aidbox (client ):
68- """HTTP client for making requests to Aidbox"""
69- app = cast (web .Application , client .server .app )
146+ @pytest .fixture
147+ async def aidbox (sdk , app ):
148+ warnings .warn (
149+ "The 'aidbox' fixture is deprecated; use 'aidbox_client' for the Aidbox client instead." ,
150+ DeprecationWarning ,
151+ stacklevel = 2 ,
152+ )
70153 basic_auth = BasicAuth (
71154 login = app [ak .settings ].APP_INIT_CLIENT_ID ,
72155 password = app [ak .settings ].APP_INIT_CLIENT_SECRET ,
73156 )
74157 session = AidboxSession (auth = basic_auth , base_url = app [ak .settings ].APP_INIT_URL )
75158 yield session
76159 await session .close ()
77-
78-
79- @pytest .fixture ()
80- async def safe_db (aidbox , client , sdk ):
81- resp = await aidbox .post (
82- "/$psql" ,
83- json = {"query" : "SELECT last_value from transaction_id_seq;" },
84- raise_for_status = True ,
85- )
86- results = await resp .json ()
87- txid = results [0 ]["result" ][0 ]["last_value" ]
88- sdk ._test_start_txid = int (txid )
89-
90- yield txid
91-
92- sdk ._test_start_txid = - 1
93- await aidbox .post (
94- "/$psql" ,
95- json = {"query" : f"select drop_before_all({ txid } );" },
96- params = {"execute" : "true" },
97- raise_for_status = True ,
98- )
99-
100-
101- @pytest .fixture ()
102- def sdk (client ):
103- return cast (web .Application , client .server .app )[ak .sdk ]
104-
105-
106- @pytest .fixture ()
107- def aidbox_client (client ):
108- return cast (web .Application , client .server .app )[ak .client ]
109-
110-
111- @pytest .fixture ()
112- def aidbox_db (client ):
113- return cast (web .Application , client .server .app )[ak .db ]
0 commit comments