@@ -975,3 +975,226 @@ async def test_connection_init_under_eager_task_factory(server):
975975 await asyncio .sleep (0 )
976976 assert conn ._disconnected is False
977977 await conn .close ()
978+
979+
980+ async def _response_handler (method : str , params : Any , is_notification : bool ) -> None :
981+ return None
982+
983+
984+ async def _start_response_requests (
985+ conn : Connection , peer : asyncio .StreamReader
986+ ) -> tuple [list [asyncio .Task [Any ]], list [dict [str , Any ]]]:
987+ tasks = [asyncio .create_task (conn .send_request ("probe" , {"index" : index })) for index in range (2 )]
988+ frames = [json .loads (await asyncio .wait_for (peer .readline (), timeout = 1 )) for _ in tasks ]
989+ assert [frame ["id" ] for frame in frames ] == [0 , 1 ]
990+ return tasks , frames
991+
992+
993+ async def _send_responses (writer : asyncio .StreamWriter , * messages : dict [str , Any ]) -> None :
994+ writer .write ("" .join (json .dumps (message ) + "\n " for message in messages ).encode ())
995+ await writer .drain ()
996+
997+
998+ async def _cleanup_response_requests (
999+ conn : Connection , tasks : list [asyncio .Task [Any ]], receive_loop : asyncio .Task [Any ] | None = None
1000+ ) -> None :
1001+ for task in tasks :
1002+ if not task .done ():
1003+ task .cancel ()
1004+ await asyncio .gather (* tasks , return_exceptions = True )
1005+ await conn .close ()
1006+ if receive_loop is not None :
1007+ if not receive_loop .done ():
1008+ receive_loop .cancel ()
1009+ await asyncio .gather (receive_loop , return_exceptions = True )
1010+
1011+
1012+ @pytest .mark .asyncio
1013+ @pytest .mark .parametrize ("error" , [["malformed" ], "malformed" , 7 , True ])
1014+ @pytest .mark .parametrize ("manual" , [False , True ], ids = ["supervised" , "main_loop" ])
1015+ async def test_bad_response_rejects_all_requests_before_close (server , error : Any , manual : bool ) -> None :
1016+ conn = Connection (_response_handler , server .client_writer , server .client_reader , listening = not manual )
1017+ receive_loop = asyncio .create_task (conn .main_loop ()) if manual else None
1018+ tasks : list [asyncio .Task [Any ]] = []
1019+ try :
1020+ tasks , frames = await _start_response_requests (conn , server .server_reader )
1021+ await _send_responses (server .server_writer , {"jsonrpc" : "2.0" , "id" : frames [0 ]["id" ], "error" : error })
1022+ done , pending = await asyncio .wait (tasks , timeout = 1 )
1023+ # Observe automatic cleanup before close(), without cancelling requests on timeout.
1024+ assert not pending , f"orphaned requests: { len (pending )} "
1025+ assert len (done ) == 2
1026+ for task in tasks :
1027+ assert isinstance (task .exception (), ConnectionError )
1028+ assert conn ._disconnected
1029+ assert not conn ._pending
1030+ if receive_loop is not None :
1031+ with pytest .raises (AttributeError ):
1032+ await receive_loop
1033+ finally :
1034+ await _cleanup_response_requests (conn , tasks , receive_loop )
1035+
1036+
1037+ @pytest .mark .asyncio
1038+ @pytest .mark .parametrize (
1039+ ("response" , "value" , "error" ),
1040+ [
1041+ ({"result" : {"value" : 1 }}, {"value" : 1 }, None ),
1042+ ({"result" : None }, None , None ),
1043+ ({"result" : False }, False , None ),
1044+ ({"result" : "ok" , "error" : ["ignored" ]}, "ok" , None ),
1045+ ({}, None , None ),
1046+ ({"error" : {"code" : - 32602 , "message" : "bad" , "data" : {"x" : 1 }}}, None , (- 32602 , "bad" , {"x" : 1 })),
1047+ * [({"error" : item }, None , (- 32603 , "Error" , None )) for item in [{}, [], "" , 0 , False , None ]],
1048+ ({"error" : {"code" : [], "message" : "Error" }}, None , ([], "Error" , None )),
1049+ ],
1050+ )
1051+ async def test_response_compatibility (server , response : dict [str , Any ], value : Any , error : Any ) -> None :
1052+ conn = Connection (_response_handler , server .client_writer , server .client_reader )
1053+ tasks : list [asyncio .Task [Any ]] = []
1054+ try :
1055+ tasks , frames = await _start_response_requests (conn , server .server_reader )
1056+ await _send_responses (
1057+ server .server_writer ,
1058+ {"jsonrpc" : "2.0" , "id" : frames [0 ]["id" ], ** response },
1059+ {"jsonrpc" : "2.0" , "id" : frames [1 ]["id" ], "result" : "survivor" },
1060+ )
1061+ _ , pending = await asyncio .wait (tasks , timeout = 1 )
1062+ assert not pending
1063+ if error is None :
1064+ assert tasks [0 ].result () == value
1065+ else :
1066+ exception = tasks [0 ].exception ()
1067+ assert isinstance (exception , RequestError )
1068+ assert (exception .code , str (exception ), exception .data ) == error
1069+ assert tasks [1 ].result () == "survivor"
1070+ assert not conn ._pending
1071+ assert not conn ._disconnected
1072+ finally :
1073+ await _cleanup_response_requests (conn , tasks )
1074+
1075+
1076+ @pytest .mark .asyncio
1077+ @pytest .mark .parametrize ("request_id" , [[], {}, True , 1.0 , 999 , "unknown" , None ])
1078+ async def test_unknown_or_invalid_ids_do_not_parse_bad_error (server , request_id : Any ) -> None :
1079+ conn = Connection (_response_handler , server .client_writer , server .client_reader )
1080+ tasks : list [asyncio .Task [Any ]] = []
1081+ try :
1082+ tasks , _ = await _start_response_requests (conn , server .server_reader )
1083+ await _send_responses (
1084+ server .server_writer ,
1085+ {"jsonrpc" : "2.0" , "id" : request_id , "error" : ["malformed" ]},
1086+ {"jsonrpc" : "2.0" , "id" : 0 , "result" : "first" },
1087+ {"jsonrpc" : "2.0" , "id" : 1 , "result" : "second" },
1088+ )
1089+ _ , pending = await asyncio .wait (tasks , timeout = 1 )
1090+ assert not pending
1091+ assert [task .result () for task in tasks ] == ["first" , "second" ]
1092+ assert not conn ._disconnected
1093+ assert not conn ._pending
1094+ finally :
1095+ await _cleanup_response_requests (conn , tasks )
1096+
1097+
1098+ @pytest .mark .asyncio
1099+ async def test_cancelled_request_and_late_bad_response (server ) -> None :
1100+ conn = Connection (_response_handler , server .client_writer , server .client_reader )
1101+ tasks : list [asyncio .Task [Any ]] = []
1102+ try :
1103+ tasks , _ = await _start_response_requests (conn , server .server_reader )
1104+ tasks [0 ].cancel ()
1105+ with pytest .raises (asyncio .CancelledError ):
1106+ await tasks [0 ]
1107+ cancel = json .loads (await asyncio .wait_for (server .server_reader .readline (), timeout = 1 ))
1108+ assert cancel ["method" ] == "$/cancel_request"
1109+ assert cancel ["params" ] == {"requestId" : 0 }
1110+ await _send_responses (
1111+ server .server_writer ,
1112+ {"jsonrpc" : "2.0" , "id" : 0 , "error" : ["malformed" ]},
1113+ {"jsonrpc" : "2.0" , "id" : 1 , "result" : "survivor" },
1114+ )
1115+ _ , pending = await asyncio .wait (tasks , timeout = 1 )
1116+ assert not pending
1117+ assert tasks [0 ].cancelled ()
1118+ assert tasks [1 ].result () == "survivor"
1119+ assert not conn ._disconnected
1120+ finally :
1121+ await _cleanup_response_requests (conn , tasks )
1122+
1123+
1124+ @pytest .mark .asyncio
1125+ async def test_duplicate_bad_response_is_ignored (server ) -> None :
1126+ conn = Connection (_response_handler , server .client_writer , server .client_reader )
1127+ tasks : list [asyncio .Task [Any ]] = []
1128+ try :
1129+ tasks , _ = await _start_response_requests (conn , server .server_reader )
1130+ await _send_responses (
1131+ server .server_writer ,
1132+ {"jsonrpc" : "2.0" , "id" : 0 , "result" : "first" },
1133+ {"jsonrpc" : "2.0" , "id" : 0 , "error" : ["malformed" ]},
1134+ {"jsonrpc" : "2.0" , "id" : 1 , "result" : "second" },
1135+ )
1136+ _ , pending = await asyncio .wait (tasks , timeout = 1 )
1137+ assert not pending
1138+ assert [task .result () for task in tasks ] == ["first" , "second" ]
1139+ assert not conn ._disconnected
1140+ finally :
1141+ await _cleanup_response_requests (conn , tasks )
1142+
1143+
1144+ @pytest .mark .asyncio
1145+ @pytest .mark .parametrize ("action" , ["close" , "eof" ])
1146+ async def test_normal_shutdown_rejects_pending (server , action : str ) -> None :
1147+ conn = Connection (_response_handler , server .client_writer , server .client_reader )
1148+ tasks : list [asyncio .Task [Any ]] = []
1149+ try :
1150+ tasks , _ = await _start_response_requests (conn , server .server_reader )
1151+ if action == "close" :
1152+ await conn .close ()
1153+ else :
1154+ server .server_writer .close ()
1155+ await server .server_writer .wait_closed ()
1156+ _ , pending = await asyncio .wait (tasks , timeout = 1 )
1157+ assert not pending
1158+ assert all (isinstance (task .exception (), ConnectionError ) for task in tasks )
1159+ assert not conn ._pending
1160+ finally :
1161+ await _cleanup_response_requests (conn , tasks )
1162+
1163+
1164+ @pytest .mark .asyncio
1165+ @pytest .mark .parametrize ("status" , ["done" , "cancelled" ])
1166+ async def test_completed_future_is_removed_without_parsing (server , status : str ) -> None :
1167+ conn = Connection (_response_handler , server .client_writer , server .client_reader , listening = False )
1168+ future = asyncio .get_running_loop ().create_future ()
1169+ conn ._pending [0 ] = future
1170+ if status == "done" :
1171+ future .set_result ("original" )
1172+ else :
1173+ future .cancel ()
1174+ try :
1175+ conn ._handle_response ({"id" : 0 , "error" : ["malformed" ]})
1176+ assert not conn ._pending
1177+ assert not conn ._disconnected
1178+ if status == "done" :
1179+ assert future .result () == "original"
1180+ else :
1181+ assert future .cancelled ()
1182+ finally :
1183+ await conn .close ()
1184+
1185+
1186+ @pytest .mark .asyncio
1187+ async def test_done_callback_runs_after_pending_removal (server ) -> None :
1188+ conn = Connection (_response_handler , server .client_writer , server .client_reader , listening = False )
1189+ future = asyncio .get_running_loop ().create_future ()
1190+ observed : list [bool ] = []
1191+ conn ._pending [0 ] = future
1192+ future .add_done_callback (lambda _ : observed .append (0 in conn ._pending ))
1193+ try :
1194+ conn ._handle_response ({"id" : 0 , "result" : "ok" })
1195+ assert observed == []
1196+ assert not conn ._pending
1197+ await asyncio .sleep (0 )
1198+ assert observed == [False ]
1199+ finally :
1200+ await conn .close ()
0 commit comments