@@ -338,53 +338,68 @@ def http_client(self):
338338
339339 client .close ()
340340
341- def test_circuit_breaker_opens_after_failures (self , http_client ):
342- """Verify the circuit opens after N consecutive failures and rejects new calls."""
341+ def test_circuit_breaker_full_lifecycle (self , http_client ):
342+ """
343+ Verifies the full lifecycle of the circuit breaker:
344+ 1. Starts closed.
345+ 2. Opens on the Nth consecutive failure.
346+ 3. Rejects new calls immediately while open.
347+ 4. Transitions to half-open after the reset timeout.
348+ 5. Closes after a single successful call in the half-open state.
349+ """
350+
343351 fail_max = 3
352+ reset_timeout = 1
344353 http_client .circuit_breaker .fail_max = fail_max
354+ http_client .circuit_breaker .reset_timeout = reset_timeout
345355
346356 with patch .object (http_client .session , "post" ) as mock_post :
347- mock_post .side_effect = requests .exceptions .RequestException ("Connection failed" )
357+ # Define the sequence of mock behaviors: 3 failures, then 1 success
358+ mock_post .side_effect = [
359+ requests .exceptions .RequestException ("Connection failed 1" ),
360+ requests .exceptions .RequestException ("Connection failed 2" ),
361+ requests .exceptions .RequestException ("Connection failed 3" ),
362+ MagicMock (ok = True , status_code = 200 ) # The successful probe call
363+ ]
348364
349- for _ in range (fail_max - 1 ):
350- with pytest .raises (requests .exceptions .RequestException ):
365+ # Cause N-1 Failures (Circuit should remain closed)
366+ # These first two calls should fail normally without opening the circuit.
367+ for i in range (fail_max - 1 ):
368+ with pytest .raises (requests .exceptions .RequestException , match = f"Connection failed { i + 1 } " ):
351369 http_client .post ("https://test.com/telemetry" )
370+
371+ # Verify state: circuit is still closed, but the counter has increased
372+ assert http_client .circuit_breaker .current_state == "closed"
373+ assert mock_post .call_count == fail_max - 1
352374
375+ # Cause the Nth Failure (This will open the circuit)
376+ # This is the call that trips the breaker. We expect a CircuitBreakerError.
353377 with pytest .raises (CircuitBreakerError ):
354378 http_client .post ("https://test.com/telemetry" )
355379
380+ # Verify state: circuit is now open and the network call was still made
356381 assert http_client .circuit_breaker .current_state == "open"
357382 assert mock_post .call_count == fail_max
358383
384+ # Verify the Circuit is Open
385+ # Any subsequent call should be rejected immediately without a network request.
359386 with pytest .raises (CircuitBreakerError ):
360387 http_client .post ("https://test.com/telemetry" )
361- assert mock_post .call_count == fail_max
362-
363- def test_circuit_breaker_closes_after_timeout_and_success (self , http_client ):
364- """Verify the circuit moves to half-open and then closes after a successful probe."""
365- fail_max = 2
366- reset_timeout = 0.1
367- http_client .circuit_breaker .fail_max = fail_max
368- http_client .circuit_breaker .reset_timeout = reset_timeout
369-
370- with patch .object (http_client .session , "post" ) as mock_post :
371- mock_post .side_effect = [
372- requests .exceptions .RequestException ("Fail 1" ),
373- requests .exceptions .RequestException ("Fail 2" ),
374- MagicMock (ok = True )
375- ]
376388
377- with pytest .raises (requests .exceptions .RequestException ):
378- http_client .post ("https://test.com" )
379- with pytest .raises (CircuitBreakerError ):
380- http_client .post ("https://test.com" )
389+ assert mock_post .call_count == fail_max
381390
382- assert http_client . circuit_breaker . current_state == "open"
391+ # Wait for the reset timeout to elapse.
383392 time .sleep (reset_timeout )
384393
385- http_client .post ("https://test.com" )
394+ # Make one more call. Since the circuit is half-open, this will be let through.
395+ # Our mock is configured for this call to succeed.
396+ http_client .post ("https://test.com/telemetry" )
397+
398+ # After the successful probe, the circuit should immediately close.
386399 assert http_client .circuit_breaker .current_state == "closed"
387- assert mock_post .call_count == 3
400+
401+ # Verify that the successful probe call was actually made
402+ assert mock_post .call_count == fail_max + 1
388403
389404 def test_circuit_breaker_reopens_if_probe_fails (self , http_client ):
390405 """Verify the circuit moves to half-open and then back to open if the probe fails."""
0 commit comments