11import os
2- import sys
32import time
43import httpx
54import pytest
1312log = get_logger ('setup-tests' )
1413
1514
15+ @pytest .fixture (scope = "session" )
16+ def anyio_backend ():
17+ return 'asyncio'
18+
19+
1620def find_free_port ():
1721 """Find a free port on localhost."""
1822 with socket .socket (socket .AF_INET , socket .SOCK_STREAM ) as s :
@@ -21,74 +25,100 @@ def find_free_port():
2125 port = s .getsockname ()[1 ]
2226 return port
2327
28+ def is_redis_reachable (tries : int = 5 , delay : float = 1.0 ) -> bool :
29+ """Check if Redis server is reachable."""
30+ for _ in range (tries ):
31+ try :
32+ redis_client .from_url ('redis://127.0.0.1:6379' ).ping ()
33+ return True
34+ except redis_client .ConnectionError :
35+ time .sleep (delay )
36+ return False
2437
25- @pytest .fixture (scope = "session" )
26- def anyio_backend ():
27- return 'asyncio'
28-
38+ def is_uvicorn_reachable (port : int , tries : int = 5 , delay : float = 1.0 , * , base_url : str = 'http://127.0.0.1' ) -> bool :
39+ """Check if Uvicorn server is reachable."""
40+ for _ in range (tries ):
41+ try :
42+ response = httpx .get (f'{ base_url } :{ port } /health' , timeout = delay * 0.8 )
43+ if response .status_code == 200 :
44+ return True
45+ except httpx .RequestError :
46+ time .sleep (delay )
47+ return False
2948
30- @pytest .fixture (scope = "session" )
31- def live_server ():
32- """Start uvicorn server in a subprocess."""
33- port = find_free_port ()
34- project_root = os .path .dirname (os .path .dirname (os .path .abspath (__file__ )))
35- processes = {}
49+ def start_redis_server (project_root : str ) -> None :
50+ print ('\n ' , end = '\r ' )
51+ log .debug ("- Starting Redis server..." )
3652
3753 try :
38- print ()
39- log .debug (f"Starting test server..." )
4054 redis_proc = subprocess .Popen (
4155 ['redis-server' , '--port' , '6379' , '--save' , '' , '--appendonly' , 'no' ],
4256 cwd = project_root ,
4357 stdout = subprocess .DEVNULL
4458 )
45- processes ['redis' ] = redis_proc
4659
47- try :
48- time . sleep ( 2 )
49- redis_client . from_url ( 'redis://127.0.0.1:6379' ). ping ()
50- except redis_client . ConnectionError :
51- log . error ( "Failed to connect to Redis server. Ensure Redis is running." )
52- raise
60+ if not is_redis_reachable () :
61+ log . error ( "x Redis server did not start successfully." )
62+ redis_proc . terminate ()
63+ raise RuntimeError ( "Could not start Redis server for tests." ) from None
64+
65+ return redis_proc
5366
67+ except Exception as e :
68+ log .error ("x Failed to start Redis server." , exc_info = e )
69+ redis_proc .kill ()
70+ raise RuntimeError ("Could not start Redis server for tests." ) from e
71+
72+ def start_uvicorn_server (port : int , project_root : str ) -> None :
73+ try :
74+ log .debug ("- Starting uvicorn server..." )
5475 uvicorn_proc = subprocess .Popen (
5576 ['uvicorn' , 'app:app' , '--host' , '127.0.0.1' , '--port' , str (port )],
5677 cwd = project_root
5778 )
79+
80+ if not is_uvicorn_reachable (port ):
81+ log .error ("x Uvicorn server did not start successfully." )
82+ uvicorn_proc .terminate ()
83+ raise RuntimeError ("Could not start Uvicorn server for tests." ) from None
84+
85+ return uvicorn_proc
86+
87+ except Exception as e :
88+ log .error ("x Failed to start Uvicorn server." , exc_info = e )
89+ uvicorn_proc .kill ()
90+ raise RuntimeError ("Could not start Uvicorn server for tests." ) from e
91+
92+ @pytest .fixture (scope = "session" )
93+ def live_server ():
94+ """Start uvicorn server in a subprocess."""
95+ port = find_free_port ()
96+ project_root = os .path .dirname (os .path .dirname (os .path .abspath (__file__ )))
97+ processes = {}
98+
99+ if not is_redis_reachable (tries = 1 ):
100+ redis_proc = start_redis_server (project_root )
101+ processes ['redis' ] = redis_proc
102+
103+ if not is_uvicorn_reachable (port , tries = 1 ):
104+ uvicorn_proc = start_uvicorn_server (port , project_root )
58105 processes ['uvicorn' ] = uvicorn_proc
59106
60- base_url = f'127.0.0.1:{ port } '
61- max_retries = 5
62- for i in range (max_retries ):
63- try :
64- response = httpx .get (f'http://{ base_url } /health' , timeout = 5 )
65- if response .status_code == 200 :
66- break
67- except Exception as e :
68- if i == max_retries - 1 :
69- uvicorn_proc .terminate ()
70- raise RuntimeError (f"Server failed to start after { max_retries } attempts" ) from None
71-
72- time .sleep (2.0 )
73-
74- log .debug (f"Server started at { base_url } " )
75- print ()
76- yield base_url
107+ yield f'127.0.0.1:{ port } '
77108
78- print ()
79- for name , process in sorted (processes .items (), key = lambda x : - ord (x [0 ][0 ])):
80- if process .poll () is None :
81- log .debug (f"Terminating { name } process" )
82- process .terminate ()
83- try :
84- process .wait (timeout = 5 )
85- except subprocess .TimeoutExpired : pass
86-
87- finally :
88- for name , process in processes .items ():
89- if process .poll () is None :
90- log .warning (f"Forcefully terminating { name } " )
91- process .kill ()
109+ print ()
110+ for name in ['uvicorn' , 'redis' ]:
111+ process = processes .get (name )
112+ if not process or process .poll () is not None :
113+ continue
114+
115+ log .debug (f"- Terminating { name } process" )
116+ process .terminate ()
117+ try :
118+ process .wait (timeout = 5 )
119+ except subprocess .TimeoutExpired :
120+ log .warning (f"- { name } process did not terminate in time, killing it" )
121+ process .kill ()
92122
93123
94124@pytest .fixture
0 commit comments