@@ -729,6 +729,18 @@ def test_negative_queue_maxsize_raises(self, mock_session) -> None:
729729 with pytest .raises (ValueError , match = "queue_maxsize must be >= 0" ):
730730 _MistWebsocket (mock_session , channels = ["/ch" ], queue_maxsize = - 1 )
731731
732+ def test_negative_max_reconnect_backoff_raises (self , mock_session ) -> None :
733+ with pytest .raises (ValueError , match = "max_reconnect_backoff must be > 0" ):
734+ _MistWebsocket (mock_session , channels = ["/ch" ], max_reconnect_backoff = - 1.0 )
735+
736+ def test_zero_max_reconnect_backoff_raises (self , mock_session ) -> None :
737+ with pytest .raises (ValueError , match = "max_reconnect_backoff must be > 0" ):
738+ _MistWebsocket (mock_session , channels = ["/ch" ], max_reconnect_backoff = 0 )
739+
740+ def test_max_reconnect_backoff_none_allowed (self , mock_session ) -> None :
741+ client = _MistWebsocket (mock_session , channels = ["/ch" ], max_reconnect_backoff = None )
742+ assert client ._max_reconnect_backoff is None
743+
732744
733745# ---------------------------------------------------------------------------
734746# Public WebSocket channel classes
@@ -987,6 +999,102 @@ def test_no_reconnect_when_disabled(self, mock_session) -> None:
987999 # run_forever called exactly once, no retry
9881000 mock_ws .run_forever .assert_called_once ()
9891001
1002+ def test_unlimited_attempts_when_max_is_zero (self , mock_session ) -> None :
1003+ """max_reconnect_attempts=0 means unlimited: loop should not stop on its own."""
1004+ client = self ._make_client (mock_session , max_reconnect_attempts = 0 )
1005+ call_count = 0
1006+ target_calls = 8 # well above any hardcoded limit
1007+
1008+ def fake_run_forever (** kwargs ):
1009+ nonlocal call_count
1010+ call_count += 1
1011+ if call_count >= target_calls :
1012+ client ._user_disconnect .set () # stop the loop
1013+ client ._handle_close (client ._ws , 1006 , "drop" )
1014+
1015+ mock_ws = Mock ()
1016+ mock_ws .run_forever .side_effect = fake_run_forever
1017+ with patch .object (client , "_create_ws_app" , return_value = mock_ws ):
1018+ client ._ws = mock_ws
1019+ client ._run_forever_safe ()
1020+
1021+ assert call_count == target_calls
1022+
1023+ def test_delay_capped_by_max_reconnect_backoff (self , mock_session ) -> None :
1024+ """When max_reconnect_backoff is set, the backoff delay never exceeds it."""
1025+ cap = 0.05
1026+ client = self ._make_client (
1027+ mock_session ,
1028+ max_reconnect_attempts = 5 ,
1029+ reconnect_backoff = 0.01 ,
1030+ max_reconnect_backoff = cap ,
1031+ )
1032+ observed_delays : list [float ] = []
1033+
1034+ original_wait = client ._user_disconnect .wait
1035+
1036+ def capture_delay (timeout = None ):
1037+ if timeout is not None :
1038+ observed_delays .append (timeout )
1039+ return original_wait (timeout = 0 ) # don't actually sleep
1040+
1041+ def fake_run_forever (** kwargs ):
1042+ client ._handle_close (client ._ws , 1006 , "drop" )
1043+
1044+ mock_ws = Mock ()
1045+ mock_ws .run_forever .side_effect = fake_run_forever
1046+ with (
1047+ patch .object (client , "_create_ws_app" , return_value = mock_ws ),
1048+ patch .object (client ._user_disconnect , "wait" , side_effect = capture_delay ),
1049+ ):
1050+ client ._ws = mock_ws
1051+ client ._run_forever_safe ()
1052+
1053+ # Should have recorded a delay for each reconnect attempt
1054+ assert len (observed_delays ) > 0
1055+ # Every observed delay must be <= cap
1056+ for delay in observed_delays :
1057+ assert delay <= cap , f"delay { delay } exceeds cap { cap } "
1058+ # Without the cap, later delays would grow via exponential backoff
1059+ # (e.g., 0.01, 0.02, 0.04, 0.08, 0.16). Verify the cap was actually
1060+ # needed by checking that at least one uncapped delay would exceed it.
1061+ uncapped = [0.01 * (2 ** i ) for i in range (len (observed_delays ))]
1062+ assert any (d > cap for d in uncapped ), "cap was never exercised"
1063+
1064+ def test_delay_uncapped_when_max_reconnect_backoff_is_none (self , mock_session ) -> None :
1065+ """Without max_reconnect_backoff, delays grow without bound."""
1066+ client = self ._make_client (
1067+ mock_session ,
1068+ max_reconnect_attempts = 4 ,
1069+ reconnect_backoff = 0.01 ,
1070+ max_reconnect_backoff = None ,
1071+ )
1072+ observed_delays : list [float ] = []
1073+
1074+ original_wait = client ._user_disconnect .wait
1075+
1076+ def capture_delay (timeout = None ):
1077+ if timeout is not None :
1078+ observed_delays .append (timeout )
1079+ return original_wait (timeout = 0 )
1080+
1081+ def fake_run_forever (** kwargs ):
1082+ client ._handle_close (client ._ws , 1006 , "drop" )
1083+
1084+ mock_ws = Mock ()
1085+ mock_ws .run_forever .side_effect = fake_run_forever
1086+ with (
1087+ patch .object (client , "_create_ws_app" , return_value = mock_ws ),
1088+ patch .object (client ._user_disconnect , "wait" , side_effect = capture_delay ),
1089+ ):
1090+ client ._ws = mock_ws
1091+ client ._run_forever_safe ()
1092+
1093+ assert len (observed_delays ) >= 2
1094+ # Each delay should be double the previous (exponential backoff)
1095+ for i in range (1 , len (observed_delays )):
1096+ assert observed_delays [i ] == pytest .approx (observed_delays [i - 1 ] * 2 )
1097+
9901098
9911099# ---------------------------------------------------------------------------
9921100# Callback exception safety
0 commit comments