@@ -91,12 +91,21 @@ async def start_server(refresh: bool, test_name: str, port: int, model: str, no_
9191
9292 @staticmethod
9393 async def stop_server (server , cache_middleware_app ):
94+ # Signal server to shut down first
95+ server .should_exit = True
96+ logger .debug ("Signaling server shutdown..." )
9497
95- await cache_middleware_app .close_resources ()
98+ # Give server a moment to start shutting down
99+ await asyncio .sleep (0.5 )
96100
97- server .should_exit = True
98- logger .debug ("Shutting down server..." )
99- await asyncio .sleep (1 )
101+ # Then close app resources
102+ try :
103+ await cache_middleware_app .close_resources ()
104+ except Exception as e :
105+ logger .warning (f"Error during resource cleanup: { e } " )
106+
107+ # Give more time for cleanup to complete
108+ await asyncio .sleep (1.5 )
100109
101110 @staticmethod
102111 async def get_attachment_url (dial_url : str , headers , attachment : Path ):
@@ -140,13 +149,22 @@ async def execute_test_case(
140149 message ["custom_content" ] = {"attachments" : attachment_objects }
141150 messages .append (message )
142151 logger .debug (f"send { message } to { client .base_url } " )
152+
153+ # Prepare request payload
154+ request_payload = {
155+ "model" : TestDialCoreConfig .APP_DEPLOYMENT_V2_NAME ,
156+ "messages" : messages ,
157+ }
158+
159+ # Add response_format if specified in test case
160+ if test_case .response_format :
161+ request_payload ["response_format" ] = test_case .response_format
162+ logger .debug (f"Using response_format: { test_case .response_format } " )
163+
143164 response = client .post (
144165 TestConfig .API_ENDPOINTS ['CHAT_COMPLETIONS' ],
145166 headers = headers ,
146- json = {
147- "model" : TestDialCoreConfig .APP_DEPLOYMENT_V2_NAME ,
148- "messages" : messages ,
149- },
167+ json = request_payload ,
150168 timeout = 100.0 ,
151169 )
152170
@@ -176,6 +194,16 @@ async def execute_test_case(
176194 break
177195
178196 logger .info (f"content:{ response_message .content } " )
197+
198+ # Validate response format if specified
199+ if test_case .response_format :
200+ format_failures = ResponseValidator .validate_json_schema_response (
201+ response_message .content , test_case .response_format , ts
202+ )
203+ if format_failures :
204+ ts .increment_failure (FailureReason .ANSWER )
205+ all_failures .extend (format_failures )
206+
179207 # Check message answer if expected
180208 if test_message_data .answer :
181209 failures = check_multiple_alternatives (
@@ -256,13 +284,18 @@ def e2e_test(
256284 test_case : TstCase = None ,
257285 app_config_path : Path = None ,
258286 model : str = None ,
259- models : List [str ] = None ,
287+ models_applicable_for_test : List [str ] = None ,
260288 refresh : bool = None ,
261289 config_file_set : str = "e2e" ,
262290 runs : int = 3 ,
291+ no_cache : bool = False ,
263292):
264293 """
265294 Decorator for end-to-end tests.
295+
296+ Args:
297+ no_cache: If True, bypass cache for this test. Can also be set globally via --no-cache CLI flag.
298+ CLI flag takes precedence over decorator parameter.
266299 """
267300
268301 if refresh is None :
@@ -281,35 +314,44 @@ async def wrapper(request, recwarn, unique_port, *args, **kwargs):
281314 f"{ test_case .name if test_case else request .node .name } "
282315 )
283316
284- execution_model_list = models if models else []
285-
286- if len (execution_model_list ) == 0 :
287- if execution_model_list :
288- execution_model_list .append (model )
289- elif request .config .getoption ("--model" ):
290- execution_model_list .append (request .config .getoption ("--model" ))
317+ model_to_use : str
318+ if model :
319+ model_to_use = model
320+ logger .debug (f"Using model from parameter defined in test: { model_to_use } " )
321+ elif request .config .getoption ("--model" ):
322+ cli_model = request .config .getoption ("--model" )
323+ if models_applicable_for_test is None or len (
324+ models_applicable_for_test ) == 0 or cli_model in models_applicable_for_test :
325+ model_to_use = cli_model
326+ logger .debug (f"Using model from CLI option: { model_to_use } " )
291327 else :
292- execution_model_list .append (TestConfig .DEFAULT_MODEL )
293-
294- for m in execution_model_list :
295- # Run the test multiple times according to the runs parameter
296- ts = TestStats (f"{ test_name } [{ m } ]" , 0 , 0 )
297- for run_index in range (runs ):
298- logger .info (f"Running test iteration { run_index + 1 } /{ runs } " )
299- failures = await prepare_and_execute_test (
300- args ,
301- kwargs ,
302- recwarn ,
303- request ,
304- unique_port ,
305- execution_model = m ,
306- test_name = test_name ,
307- test_stats = ts ,
308- run_index = run_index ,
309- )
310- all_runs_failures .extend (failures )
311- logger .info (ts )
312- report_test_stats (request .config , ts )
328+ logger .debug (
329+ f"Model '{ cli_model } ' is not in the applicable models list: { models_applicable_for_test } " )
330+ pytest .skip (f"Model '{ cli_model } ' is not applicable for this test" )
331+ else :
332+ logger .debug ("No model specified" )
333+ pytest .fail ("No model specified for test" )
334+
335+
336+
337+ # Run the test multiple times according to the runs parameter
338+ ts = TestStats (f"{ test_name } [{ model_to_use } ]" , 0 , 0 )
339+ for run_index in range (runs ):
340+ logger .info (f"Running test iteration { run_index + 1 } /{ runs } " )
341+ failures = await prepare_and_execute_test (
342+ args ,
343+ kwargs ,
344+ recwarn ,
345+ request ,
346+ unique_port ,
347+ execution_model = model_to_use ,
348+ test_name = test_name ,
349+ test_stats = ts ,
350+ run_index = run_index ,
351+ )
352+ all_runs_failures .extend (failures )
353+ logger .info (ts )
354+ report_test_stats (request .config , ts )
313355
314356 # After all runs/models are complete, check if any failures occurred
315357 TestRunner .check_test_outcome (all_runs_failures )
@@ -334,13 +376,16 @@ async def prepare_and_execute_test(
334376
335377 client = TestClient (app )
336378
337- no_cache = bool (request .config .getoption ("--no-cache" , default = False ))
379+ # Combine CLI flag with decorator parameter - CLI takes precedence
380+ cli_no_cache = bool (request .config .getoption ("--no-cache" , default = False ))
381+ effective_no_cache = cli_no_cache or no_cache
382+
338383 task , server , middleware = await TestRunner .start_server (
339384 model = execution_model ,
340385 test_name = test_name ,
341386 refresh = refresh ,
342387 port = unique_port ,
343- no_cache = no_cache
388+ no_cache = effective_no_cache
344389 )
345390 try :
346391 run_failures , test_result = await execute_single_test_run (
@@ -362,13 +407,8 @@ async def prepare_and_execute_test(
362407
363408 finally :
364409 await TestRunner .stop_server (server , middleware )
365- # Properly close the client
366- if hasattr (client , "aclose" ):
367- await client .aclose ()
368- # Shutdown async generators
369- loop = asyncio .get_event_loop ()
370- if loop .is_running ():
371- await loop .shutdown_asyncgens ()
410+ # TestClient is synchronous and doesn't need async close
411+ # Don't shutdown async generators while loop is running
372412
373413 return wrapper
374414
0 commit comments