1- import pytest
2- from unittest .mock import Mock , patch
31from typing import Any
2+ from unittest .mock import Mock , patch
3+
4+ import pytest
45
5- from src .runloop_api_client .lib .polling import poll_until , PollingConfig , PollingTimeout
6+ from src .runloop_api_client .lib .polling import PollingConfig , PollingTimeout , poll_until
67
78
89class TestPollingConfig :
910 """Test PollingConfig dataclass"""
10-
11+
1112 def test_default_config (self ):
1213 config = PollingConfig ()
1314 assert config .interval_seconds == 1.0
1415 assert config .max_attempts == 120
1516 assert config .timeout_seconds is None
16-
17+
1718 def test_custom_config (self ):
18- config = PollingConfig (
19- interval_seconds = 0.5 ,
20- max_attempts = 10 ,
21- timeout_seconds = 30.0
22- )
19+ config = PollingConfig (interval_seconds = 0.5 , max_attempts = 10 , timeout_seconds = 30.0 )
2320 assert config .interval_seconds == 0.5
2421 assert config .max_attempts == 10
2522 assert config .timeout_seconds == 30.0
2623
2724
2825class TestPollingTimeout :
2926 """Test PollingTimeout exception"""
30-
27+
3128 def test_polling_timeout_initialization (self ):
3229 last_value = {"status" : "running" }
3330 exception = PollingTimeout ("Test message" , last_value )
34-
31+
3532 assert exception .last_value == last_value
3633 assert "Test message" in str (exception )
3734 assert "Last retrieved value: {'status': 'running'}" in str (exception )
3835
3936
4037class TestPollUntil :
4138 """Test poll_until function"""
42-
39+
4340 def test_immediate_success (self ):
4441 """Test when condition is met on first attempt"""
4542 retriever = Mock (return_value = "completed" )
4643 is_terminal = Mock (return_value = True )
47-
44+
4845 result = poll_until (retriever , is_terminal )
49-
46+
5047 assert result == "completed"
5148 assert retriever .call_count == 1
5249 assert is_terminal .call_count == 1
5350 is_terminal .assert_called_with ("completed" )
54-
51+
5552 def test_success_after_multiple_attempts (self ):
5653 """Test when condition is met after several attempts"""
5754 values = ["pending" , "running" , "completed" ]
5855 retriever = Mock (side_effect = values )
5956 is_terminal = Mock (side_effect = [False , False , True ])
60-
61- with patch (' time.sleep' ) as mock_sleep :
57+
58+ with patch (" time.sleep" ) as mock_sleep :
6259 result = poll_until (retriever , is_terminal )
63-
60+
6461 assert result == "completed"
6562 assert retriever .call_count == 3
6663 assert is_terminal .call_count == 3
6764 assert mock_sleep .call_count == 2 # Should sleep between attempts
68-
65+
6966 def test_custom_config_interval (self ):
7067 """Test with custom polling interval"""
7168 retriever = Mock (side_effect = ["pending" , "completed" ])
7269 is_terminal = Mock (side_effect = [False , True ])
7370 config = PollingConfig (interval_seconds = 0.1 )
74-
75- with patch (' time.sleep' ) as mock_sleep :
71+
72+ with patch (" time.sleep" ) as mock_sleep :
7673 result = poll_until (retriever , is_terminal , config )
77-
74+
7875 assert result == "completed"
7976 mock_sleep .assert_called_with (0.1 )
80-
77+
8178 def test_max_attempts_exceeded (self ):
8279 """Test when max attempts is exceeded"""
8380 retriever = Mock (return_value = "still_running" )
8481 is_terminal = Mock (return_value = False )
8582 config = PollingConfig (max_attempts = 3 , interval_seconds = 0.01 )
86-
87- with patch (' time.sleep' ):
83+
84+ with patch (" time.sleep" ):
8885 with pytest .raises (PollingTimeout ) as exc_info :
8986 poll_until (retriever , is_terminal , config )
90-
87+
9188 assert "Exceeded maximum attempts (3)" in str (exc_info .value )
9289 assert exc_info .value .last_value == "still_running"
9390 assert retriever .call_count == 3
94-
91+
9592 def test_timeout_exceeded (self ):
9693 """Test when timeout is exceeded"""
9794 retriever = Mock (return_value = "still_running" )
9895 is_terminal = Mock (return_value = False )
9996 config = PollingConfig (timeout_seconds = 0.1 , interval_seconds = 0.01 )
100-
97+
10198 # Mock time.time to simulate timeout
10299 start_time = 1000.0
103- with patch (' time.time' , side_effect = [start_time , start_time + 0.05 , start_time + 0.15 ]):
104- with patch (' time.sleep' ):
100+ with patch (" time.time" , side_effect = [start_time , start_time + 0.05 , start_time + 0.15 ]):
101+ with patch (" time.sleep" ):
105102 with pytest .raises (PollingTimeout ) as exc_info :
106103 poll_until (retriever , is_terminal , config )
107-
104+
108105 assert "Exceeded timeout of 0.1 seconds" in str (exc_info .value )
109106 assert exc_info .value .last_value == "still_running"
110-
107+
111108 def test_error_without_handler (self ):
112109 """Test that exceptions are re-raised when no error handler is provided"""
113110 retriever = Mock (side_effect = ValueError ("Test error" ))
114111 is_terminal = Mock (return_value = False )
115-
112+
116113 with pytest .raises (ValueError , match = "Test error" ):
117114 poll_until (retriever , is_terminal )
118-
115+
119116 def test_error_with_handler_continue (self ):
120117 """Test error handler that allows polling to continue"""
121118 retriever = Mock (side_effect = [ValueError ("Test error" ), "recovered" ])
122119 is_terminal = Mock (side_effect = [False , True ])
123-
124- def error_handler (e : Exception ) -> str :
120+
121+ def error_handler (_ : Exception ) -> str :
125122 return "error_handled"
126-
127- with patch (' time.sleep' ):
123+
124+ with patch (" time.sleep" ):
128125 result = poll_until (retriever , is_terminal , on_error = error_handler )
129-
126+
130127 assert result == "recovered"
131128 assert retriever .call_count == 2
132129 assert is_terminal .call_count == 2
133-
130+
134131 def test_error_with_handler_reraise (self ):
135132 """Test error handler that re-raises the exception"""
136133 retriever = Mock (side_effect = ValueError ("Test error" ))
137134 is_terminal = Mock (return_value = False )
138-
135+
139136 def error_handler (e : Exception ) -> None :
140137 raise e
141-
138+
142139 with pytest .raises (ValueError , match = "Test error" ):
143140 poll_until (retriever , is_terminal , on_error = error_handler )
144-
141+
145142 def test_error_handler_return_terminal_value (self ):
146143 """Test error handler that returns a terminal value"""
147144 retriever = Mock (side_effect = ValueError ("Test error" ))
148145 is_terminal = Mock (side_effect = [True ]) # Terminal condition met on error handler return
149-
150- def error_handler (e : Exception ) -> str :
146+
147+ def error_handler (_ : Exception ) -> str :
151148 return "error_terminal"
152-
149+
153150 result = poll_until (retriever , is_terminal , on_error = error_handler )
154-
151+
155152 assert result == "error_terminal"
156153 assert retriever .call_count == 1
157154 assert is_terminal .call_count == 1
158-
155+
159156 def test_multiple_errors_with_handler (self ):
160157 """Test multiple errors with handler"""
161- retriever = Mock (side_effect = [
162- ValueError ("Error 1" ),
163- RuntimeError ("Error 2" ),
164- "success"
165- ])
158+ retriever = Mock (side_effect = [ValueError ("Error 1" ), RuntimeError ("Error 2" ), "success" ])
166159 is_terminal = Mock (side_effect = [False , False , True ])
167-
160+
168161 error_count = 0
169- def error_handler (e : Exception ) -> str :
162+
163+ def error_handler (_ : Exception ) -> str :
170164 nonlocal error_count
171165 error_count += 1
172166 return f"handled_error_{ error_count } "
173-
174- with patch (' time.sleep' ):
167+
168+ with patch (" time.sleep" ):
175169 result = poll_until (retriever , is_terminal , on_error = error_handler )
176-
170+
177171 assert result == "success"
178172 assert error_count == 2
179173 assert retriever .call_count == 3
180-
174+
181175 def test_none_values_handling (self ):
182176 """Test handling of None values"""
183177 retriever = Mock (side_effect = [None , None , "final" ])
184178 is_terminal = Mock (side_effect = [False , False , True ])
185-
186- with patch (' time.sleep' ):
179+
180+ with patch (" time.sleep" ):
187181 result = poll_until (retriever , is_terminal )
188-
182+
189183 assert result == "final"
190184 assert retriever .call_count == 3
191-
185+
192186 def test_complex_object_polling (self ):
193187 """Test polling with complex objects"""
188+
194189 class Status :
195190 def __init__ (self , state : str , progress : int ):
196191 self .state = state
197192 self .progress = progress
198-
199- statuses = [
200- Status ("starting" , 0 ),
201- Status ("running" , 50 ),
202- Status ("completed" , 100 )
203- ]
204-
193+
194+ statuses = [Status ("starting" , 0 ), Status ("running" , 50 ), Status ("completed" , 100 )]
195+
205196 retriever = Mock (side_effect = statuses )
206197 is_terminal = Mock (side_effect = [False , False , True ])
207-
208- with patch (' time.sleep' ):
198+
199+ with patch (" time.sleep" ):
209200 result = poll_until (retriever , is_terminal )
210-
201+
211202 assert result .state == "completed"
212203 assert result .progress == 100
213-
204+
214205 def test_zero_max_attempts (self ):
215206 """Test with zero max attempts"""
216207 retriever = Mock (return_value = "value" )
217208 is_terminal = Mock (return_value = False )
218209 config = PollingConfig (max_attempts = 0 )
219-
210+
220211 with pytest .raises (PollingTimeout ) as exc_info :
221212 poll_until (retriever , is_terminal , config )
222-
213+
223214 assert "Exceeded maximum attempts (0)" in str (exc_info .value )
224215 assert retriever .call_count == 1 # Retriever is called once, then attempts check happens
225-
216+
226217 def test_negative_interval (self ):
227218 """Test with negative interval (should still work)"""
228219 retriever = Mock (side_effect = ["first" , "second" ])
229220 is_terminal = Mock (side_effect = [False , True ])
230221 config = PollingConfig (interval_seconds = - 0.1 )
231-
232- with patch (' time.sleep' ) as mock_sleep :
222+
223+ with patch (" time.sleep" ) as mock_sleep :
233224 result = poll_until (retriever , is_terminal , config )
234-
225+
235226 assert result == "second"
236227 mock_sleep .assert_called_with (- 0.1 )
237-
228+
238229 def test_both_timeout_and_max_attempts (self ):
239230 """Test when both timeout and max_attempts are set"""
240231 retriever = Mock (return_value = "still_running" )
241232 is_terminal = Mock (return_value = False )
242233 config = PollingConfig (max_attempts = 5 , timeout_seconds = 0.1 , interval_seconds = 0.01 )
243-
234+
244235 # Mock time to hit timeout before max_attempts
245236 start_time = 1000.0
246- with patch (' time.time' , side_effect = [start_time , start_time + 0.05 , start_time + 0.15 ]):
247- with patch (' time.sleep' ):
237+ with patch (" time.time" , side_effect = [start_time , start_time + 0.05 , start_time + 0.15 ]):
238+ with patch (" time.sleep" ):
248239 with pytest .raises (PollingTimeout ) as exc_info :
249240 poll_until (retriever , is_terminal , config )
250-
241+
251242 # Should hit timeout first
252243 assert "Exceeded timeout of 0.1 seconds" in str (exc_info .value )
253244 assert retriever .call_count == 2 # Called twice before timeout
254-
245+
255246 def test_terminal_condition_changes (self ):
256247 """Test when terminal condition logic changes during polling"""
257248 retriever = Mock (side_effect = ["value1" , "value2" , "value3" ])
258-
249+
259250 call_count = 0
260- def dynamic_terminal (value : Any ) -> bool :
251+
252+ def dynamic_terminal (_ : Any ) -> bool :
261253 nonlocal call_count
262254 call_count += 1
263255 # First two calls return False, third returns True
264256 return call_count >= 3
265-
266- with patch (' time.sleep' ):
257+
258+ with patch (" time.sleep" ):
267259 result = poll_until (retriever , dynamic_terminal )
268-
260+
269261 assert result == "value3"
270- assert retriever .call_count == 3
262+ assert retriever .call_count == 3
0 commit comments