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 ) {
@@ -294,7 +295,7 @@ func TestBackOffDelay(t *testing.T) {
294295 delay : - 1 ,
295296 expectedMaxN : 62 ,
296297 n : 2 ,
297- expectedDelay : 4 ,
298+ expectedDelay : 2 ,
298299 },
299300 {
300301 label : "zero-delay" ,
@@ -310,6 +311,13 @@ func TestBackOffDelay(t *testing.T) {
310311 n : 62 ,
311312 expectedDelay : time .Second << 33 ,
312313 },
314+ {
315+ label : "one-second-n" ,
316+ delay : time .Second ,
317+ expectedMaxN : 33 ,
318+ n : 1 ,
319+ expectedDelay : time .Second ,
320+ },
313321 } {
314322 t .Run (
315323 c .label ,
@@ -490,7 +498,7 @@ func TestContext(t *testing.T) {
490498 })
491499
492500 t .Run ("timed out on retry infinte attempts - wraps context error with last retried function error" , func (t * testing.T ) {
493- ctx , cancel := context .WithTimeout (context .Background (), time .Millisecond * 500 )
501+ ctx , cancel := context .WithTimeout (context .Background (), time .Millisecond * 200 )
494502 defer cancel ()
495503
496504 retrySum := 0
@@ -633,6 +641,74 @@ func BenchmarkDoWithDataNoErrors(b *testing.B) {
633641 }
634642}
635643
644+ type attemptsForErrorTestError struct {}
645+
646+ func (attemptsForErrorTestError ) Error () string { return "test error" }
647+
648+ func TestAttemptsForErrorNoDelayAfterFinalAttempt (t * testing.T ) {
649+ var count uint64
650+ var timestamps []time.Time
651+
652+ startTime := time .Now ()
653+
654+ err := Do (
655+ func () error {
656+ count ++
657+ timestamps = append (timestamps , time .Now ())
658+ return attemptsForErrorTestError {}
659+ },
660+ Attempts (3 ),
661+ Delay (200 * time .Millisecond ),
662+ DelayType (FixedDelay ),
663+ AttemptsForError (2 , attemptsForErrorTestError {}),
664+ LastErrorOnly (true ),
665+ Context (context .Background ()),
666+ )
667+
668+ endTime := time .Now ()
669+
670+ assert .Error (t , err )
671+ assert .Equal (t , uint64 (2 ), count , "should attempt exactly 2 times" )
672+ assert .Len (t , timestamps , 2 , "should have 2 timestamps" )
673+
674+ // Verify timing: first attempt at ~0ms, second at ~200ms, end immediately after second attempt
675+ firstAttemptTime := timestamps [0 ].Sub (startTime )
676+ secondAttemptTime := timestamps [1 ].Sub (startTime )
677+ totalTime := endTime .Sub (startTime )
678+
679+ // First attempt should be immediate
680+ assert .Less (t , firstAttemptTime , 50 * time .Millisecond , "first attempt should be immediate" )
681+
682+ // Second attempt should be after delay
683+ assert .Greater (t , secondAttemptTime , 150 * time .Millisecond , "second attempt should be after delay" )
684+ assert .Less (t , secondAttemptTime , 250 * time .Millisecond , "second attempt should not be too delayed" )
685+
686+ // Total time should not include delay after final attempt
687+ assert .Less (t , totalTime , 300 * time .Millisecond , "should not delay after final attempt" )
688+ }
689+
690+ func TestOnRetryNotCalledOnLastAttempt (t * testing.T ) {
691+ callCount := 0
692+ onRetryCalls := make ([]uint , 0 )
693+
694+ err := Do (
695+ func () error {
696+ callCount ++
697+ return errors .New ("test error" )
698+ },
699+ Attempts (3 ),
700+ OnRetry (func (n uint , err error ) {
701+ onRetryCalls = append (onRetryCalls , n )
702+ }),
703+ Delay (time .Nanosecond ),
704+ )
705+
706+ assert .Error (t , err )
707+ assert .Equal (t , 3 , callCount , "function should be called 3 times" )
708+ assert .Equal (t , []uint {0 , 1 }, onRetryCalls , "onRetry should only be called for first 2 attempts, not the final one" )
709+ assert .Len (t , onRetryCalls , 2 , "onRetry should be called exactly 2 times (not on last attempt)" )
710+ }
711+
636712func TestIsRecoverable (t * testing.T ) {
637713 err := errors .New ("err" )
638714 assert .True (t , IsRecoverable (err ))
@@ -643,3 +719,71 @@ func TestIsRecoverable(t *testing.T) {
643719 err = fmt .Errorf ("wrapping: %w" , err )
644720 assert .False (t , IsRecoverable (err ))
645721}
722+
723+ func TestFullJitterBackoffDelay (t * testing.T ) {
724+ // Seed for predictable randomness in tests
725+ // In real usage, math/rand is auto-seeded in Go 1.20+ or should be seeded once at program start.
726+ // For library test predictability, local seeding is fine.
727+ // However, retry-go's RandomDelay uses global math/rand without explicit seeding in tests.
728+ // Let's follow the existing pattern of not explicitly seeding in each test for now,
729+ // assuming test runs are isolated enough or that exact delay values aren't asserted,
730+ // but rather ranges or properties.
731+
732+ baseDelay := 50 * time .Millisecond
733+ maxDelay := 500 * time .Millisecond
734+
735+ config := & Config {
736+ delay : baseDelay ,
737+ maxDelay : maxDelay ,
738+ // other fields can be zero/default for this test
739+ }
740+
741+ attempts := []uint {0 , 1 , 2 , 3 , 4 , 5 , 6 , 10 }
742+
743+ for _ , n := range attempts {
744+ delay := FullJitterBackoffDelay (n , errors .New ("test error" ), config )
745+
746+ expectedMaxCeiling := float64 (baseDelay ) * math .Pow (2 , float64 (n ))
747+ if expectedMaxCeiling > float64 (maxDelay ) {
748+ expectedMaxCeiling = float64 (maxDelay )
749+ }
750+
751+ assert .True (t , delay >= 0 , "Delay should be non-negative. Got: %v for attempt %d" , delay , n )
752+ assert .True (t , delay <= time .Duration (expectedMaxCeiling ),
753+ "Delay %v should be less than or equal to current backoff ceiling %v for attempt %d" , delay , time .Duration (expectedMaxCeiling ), n )
754+
755+ t .Logf ("Attempt %d: BaseDelay=%v, MaxDelay=%v, Calculated Ceiling=~%v, Actual Delay=%v" ,
756+ n , baseDelay , maxDelay , time .Duration (expectedMaxCeiling ), delay )
757+
758+ // Test with MaxDelay disabled (0)
759+ configNoMax := & Config {delay : baseDelay , maxDelay : 0 }
760+ delayNoMax := FullJitterBackoffDelay (n , errors .New ("test error" ), configNoMax )
761+ expectedCeilingNoMax := float64 (baseDelay ) * math .Pow (2 , float64 (n ))
762+ if expectedCeilingNoMax > float64 (10 * time .Minute ) { // Avoid overflow for very large N
763+ expectedCeilingNoMax = float64 (10 * time .Minute )
764+ }
765+ assert .True (t , delayNoMax >= 0 , "Delay (no max) should be non-negative. Got: %v for attempt %d" , delayNoMax , n )
766+ assert .True (t , delayNoMax <= time .Duration (expectedCeilingNoMax ),
767+ "Delay (no max) %v should be less than or equal to current backoff ceiling %v for attempt %d" , delayNoMax , time .Duration (expectedCeilingNoMax ), n )
768+ }
769+
770+ // Test case where baseDelay might be zero
771+ configZeroBase := & Config {delay : 0 , maxDelay : maxDelay }
772+ delayZeroBase := FullJitterBackoffDelay (0 , errors .New ("test error" ), configZeroBase )
773+ assert .Equal (t , time .Duration (0 ), delayZeroBase , "Delay with zero base delay should be 0" )
774+
775+ delayZeroBaseAttempt1 := FullJitterBackoffDelay (1 , errors .New ("test error" ), configZeroBase )
776+ assert .Equal (t , time .Duration (0 ), delayZeroBaseAttempt1 , "Delay with zero base delay (attempt > 0) should be 0" )
777+
778+ // Test with very small base delay
779+ smallBaseDelay := 1 * time .Nanosecond
780+ configSmallBase := & Config {delay : smallBaseDelay , maxDelay : 100 * time .Nanosecond }
781+ for i := uint (0 ); i < 5 ; i ++ {
782+ d := FullJitterBackoffDelay (i , errors .New ("test" ), configSmallBase )
783+ ceil := float64 (smallBaseDelay ) * math .Pow (2 , float64 (i ))
784+ if ceil > 100 {
785+ ceil = 100
786+ }
787+ assert .True (t , d <= time .Duration (ceil ))
788+ }
789+ }
0 commit comments