44 "context"
55 "errors"
66 "fmt"
7+ "math"
78 "os"
89 "testing"
910 "time"
@@ -35,7 +36,7 @@ func TestDoWithDataAllFailed(t *testing.T) {
3536 assert .Len (t , err , 10 )
3637 fmt .Println (err .Error ())
3738 assert .Equal (t , expectedErrorFormat , err .Error (), "retry error format" )
38- assert .Equal (t , uint (45 ), retrySum , "right count of retry" )
39+ assert .Equal (t , uint (36 ), retrySum , "right count of retry" )
3940}
4041
4142func TestDoFirstOk (t * testing.T ) {
@@ -678,6 +679,28 @@ func TestAttemptsForErrorNoDelayAfterFinalAttempt(t *testing.T) {
678679 assert .Less (t , totalTime , 300 * time .Millisecond , "should not delay after final attempt" )
679680}
680681
682+ func TestOnRetryNotCalledOnLastAttempt (t * testing.T ) {
683+ callCount := 0
684+ onRetryCalls := make ([]uint , 0 )
685+
686+ err := Do (
687+ func () error {
688+ callCount ++
689+ return errors .New ("test error" )
690+ },
691+ Attempts (3 ),
692+ OnRetry (func (n uint , err error ) {
693+ onRetryCalls = append (onRetryCalls , n )
694+ }),
695+ Delay (time .Nanosecond ),
696+ )
697+
698+ assert .Error (t , err )
699+ assert .Equal (t , 3 , callCount , "function should be called 3 times" )
700+ assert .Equal (t , []uint {0 , 1 }, onRetryCalls , "onRetry should only be called for first 2 attempts, not the final one" )
701+ assert .Len (t , onRetryCalls , 2 , "onRetry should be called exactly 2 times (not on last attempt)" )
702+ }
703+
681704func TestIsRecoverable (t * testing.T ) {
682705 err := errors .New ("err" )
683706 assert .True (t , IsRecoverable (err ))
@@ -688,3 +711,71 @@ func TestIsRecoverable(t *testing.T) {
688711 err = fmt .Errorf ("wrapping: %w" , err )
689712 assert .False (t , IsRecoverable (err ))
690713}
714+
715+ func TestFullJitterBackoffDelay (t * testing.T ) {
716+ // Seed for predictable randomness in tests
717+ // In real usage, math/rand is auto-seeded in Go 1.20+ or should be seeded once at program start.
718+ // For library test predictability, local seeding is fine.
719+ // However, retry-go's RandomDelay uses global math/rand without explicit seeding in tests.
720+ // Let's follow the existing pattern of not explicitly seeding in each test for now,
721+ // assuming test runs are isolated enough or that exact delay values aren't asserted,
722+ // but rather ranges or properties.
723+
724+ baseDelay := 50 * time .Millisecond
725+ maxDelay := 500 * time .Millisecond
726+
727+ config := & Config {
728+ delay : baseDelay ,
729+ maxDelay : maxDelay ,
730+ // other fields can be zero/default for this test
731+ }
732+
733+ attempts := []uint {0 , 1 , 2 , 3 , 4 , 5 , 6 , 10 }
734+
735+ for _ , n := range attempts {
736+ delay := FullJitterBackoffDelay (n , errors .New ("test error" ), config )
737+
738+ expectedMaxCeiling := float64 (baseDelay ) * math .Pow (2 , float64 (n ))
739+ if expectedMaxCeiling > float64 (maxDelay ) {
740+ expectedMaxCeiling = float64 (maxDelay )
741+ }
742+
743+ assert .True (t , delay >= 0 , "Delay should be non-negative. Got: %v for attempt %d" , delay , n )
744+ assert .True (t , delay <= time .Duration (expectedMaxCeiling ),
745+ "Delay %v should be less than or equal to current backoff ceiling %v for attempt %d" , delay , time .Duration (expectedMaxCeiling ), n )
746+
747+ t .Logf ("Attempt %d: BaseDelay=%v, MaxDelay=%v, Calculated Ceiling=~%v, Actual Delay=%v" ,
748+ n , baseDelay , maxDelay , time .Duration (expectedMaxCeiling ), delay )
749+
750+ // Test with MaxDelay disabled (0)
751+ configNoMax := & Config {delay : baseDelay , maxDelay : 0 }
752+ delayNoMax := FullJitterBackoffDelay (n , errors .New ("test error" ), configNoMax )
753+ expectedCeilingNoMax := float64 (baseDelay ) * math .Pow (2 , float64 (n ))
754+ if expectedCeilingNoMax > float64 (10 * time .Minute ) { // Avoid overflow for very large N
755+ expectedCeilingNoMax = float64 (10 * time .Minute )
756+ }
757+ assert .True (t , delayNoMax >= 0 , "Delay (no max) should be non-negative. Got: %v for attempt %d" , delayNoMax , n )
758+ assert .True (t , delayNoMax <= time .Duration (expectedCeilingNoMax ),
759+ "Delay (no max) %v should be less than or equal to current backoff ceiling %v for attempt %d" , delayNoMax , time .Duration (expectedCeilingNoMax ), n )
760+ }
761+
762+ // Test case where baseDelay might be zero
763+ configZeroBase := & Config {delay : 0 , maxDelay : maxDelay }
764+ delayZeroBase := FullJitterBackoffDelay (0 , errors .New ("test error" ), configZeroBase )
765+ assert .Equal (t , time .Duration (0 ), delayZeroBase , "Delay with zero base delay should be 0" )
766+
767+ delayZeroBaseAttempt1 := FullJitterBackoffDelay (1 , errors .New ("test error" ), configZeroBase )
768+ assert .Equal (t , time .Duration (0 ), delayZeroBaseAttempt1 , "Delay with zero base delay (attempt > 0) should be 0" )
769+
770+ // Test with very small base delay
771+ smallBaseDelay := 1 * time .Nanosecond
772+ configSmallBase := & Config {delay : smallBaseDelay , maxDelay : 100 * time .Nanosecond }
773+ for i := uint (0 ); i < 5 ; i ++ {
774+ d := FullJitterBackoffDelay (i , errors .New ("test" ), configSmallBase )
775+ ceil := float64 (smallBaseDelay ) * math .Pow (2 , float64 (i ))
776+ if ceil > 100 {
777+ ceil = 100
778+ }
779+ assert .True (t , d <= time .Duration (ceil ))
780+ }
781+ }
0 commit comments