@@ -55,23 +55,21 @@ func hardDeadlockTimeout() time.Duration {
5555// test failure. Use t.Context() inside the callback to honor the timeout.
5656func Require (ctx context.Context , tb testing.TB , condition func (* T ), timeout , pollInterval time.Duration ) {
5757 tb .Helper ()
58- run (ctx , tb , condition , timeout , pollInterval , "" , "Require" , requireMisuseHint , true )
58+ run (ctx , tb , condition , legacyConfig ( timeout , pollInterval , "" ) , "Require" , requireMisuseHint , true )
5959}
6060
6161// Requiref is like [Require] but adds a formatted message to the timeout
6262// failure.
6363func Requiref (ctx context.Context , tb testing.TB , condition func (* T ), timeout , pollInterval time.Duration , msg string , args ... any ) {
6464 tb .Helper ()
65- run (ctx , tb , condition , timeout , pollInterval , fmt .Sprintf (msg , args ... ), "Requiref" , requireMisuseHint , true )
65+ run (ctx , tb , condition , legacyConfig ( timeout , pollInterval , fmt .Sprintf (msg , args ... ) ), "Requiref" , requireMisuseHint , true )
6666}
6767
6868func run (
6969 parentCtx context.Context ,
7070 tb testing.TB ,
7171 condition func (* T ),
72- timeout ,
73- pollInterval time.Duration ,
74- timeoutMsg string ,
72+ cfg config ,
7573 funcName string ,
7674 misuseHint string ,
7775 cancellable bool ,
@@ -89,7 +87,7 @@ func run(
8987 return
9088 }
9189
92- deadline := time .Now ().Add (timeout )
90+ deadline := time .Now ().Add (cfg . totalTimeout )
9391
9492 // Cap at the parent context's deadline if it's earlier than our timeout.
9593 if parentDeadline , hasDeadline := parentCtx .Deadline (); hasDeadline && parentDeadline .Before (deadline ) {
@@ -108,22 +106,21 @@ func run(
108106 awaitCtx , awaitCancel := context .WithDeadline (parentCtx , deadline )
109107 defer awaitCancel ()
110108
111- var failures []attemptFailure
112- polls := 0
109+ report := timeoutReport {effectiveTimeout : effectiveTimeout }
113110
114111 for {
115112 // Parent context was canceled while we were sleeping (not our deadline).
116113 if err := awaitCtx .Err (); err != nil && ! deadlineReached (deadline ) {
117- reportAttemptErrors (tb , failures )
114+ report . reportAttemptErrors (tb )
118115 tb .Fatalf ("%s: context canceled before condition was satisfied: %v" , funcName , err )
119116 return
120117 }
121118
122- polls ++
119+ report . nextPoll ()
123120
124- // Fresh context per attempt, scoped to the run-level ctx. runAttempt
125- // owns the soft timeout and the corresponding cancel .
126- attemptCtx , attemptCancel := context .WithCancel (awaitCtx )
121+ // Per-attempt context: bounded by the configured attempt timeout and
122+ // further capped by the overall awaitCtx .
123+ attemptCtx , attemptCancel := context .WithTimeout (awaitCtx , cfg . attemptTimeout )
127124 t := & T {tb : tb , ctx : attemptCtx }
128125
129126 // Run attempt.
@@ -133,18 +130,24 @@ func run(
133130 panic (res .panicVal ) // propagate to caller
134131 }
135132 if res .deadlocked {
136- reportAttemptErrors (tb , failures )
133+ report . reportAttemptErrors (tb )
137134 if cancellable {
138- tb .Fatalf ("%s: condition still running %v past context cancellation — does it honor t.Context()? (%d polls )" ,
139- funcName , hardDeadlockTimeout (), polls )
135+ tb .Fatalf ("%s: condition still running %v past context cancellation — does it honor t.Context()? (%d attempts )" ,
136+ funcName , hardDeadlockTimeout (), report . attempts )
140137 } else {
141- tb .Fatalf ("%s: condition still running %v past deadline (%d polls )" ,
142- funcName , hardDeadlockTimeout (), polls )
138+ tb .Fatalf ("%s: condition still running %v past deadline (%d attempts )" ,
139+ funcName , hardDeadlockTimeout (), report . attempts )
143140 }
144141 return
145142 }
146- if len (t .errors ) > 0 {
147- failures = append (failures , attemptFailure {attempt : polls , errors : t .errors })
143+ report .recordErrors (t .errors )
144+
145+ // Attempt-timeout expiry: attemptCtx is done but awaitCtx is not.
146+ // Record nothing special - the attempt's recorded errors (if any)
147+ // already describe what went wrong; otherwise we just retry.
148+ attemptHitOwnTimeout := attemptCtx .Err () == context .DeadlineExceeded && awaitCtx .Err () == nil
149+ if attemptHitOwnTimeout {
150+ report .recordAttemptTimeout ()
148151 }
149152
150153 // Check misuse where the real test failed instead of just the attempt.
@@ -155,24 +158,24 @@ func run(
155158
156159 // Parent context was canceled during the attempt (not our deadline).
157160 if err := awaitCtx .Err (); err != nil && ! deadlineReached (deadline ) {
158- reportAttemptErrors (tb , failures )
161+ report . reportAttemptErrors (tb )
159162 tb .Fatalf ("%s: context canceled before condition was satisfied: %v" , funcName , err )
160163 return
161164 }
162165
163166 // Our deadline expired.
164167 if deadlineReached (deadline ) {
165- reportTimeout (tb , failures , funcName , timeoutMsg , effectiveTimeout , polls )
168+ report . reportTimeout (tb , funcName , cfg . timeoutMsg )
166169 return
167170 }
168171
169172 // Success: attempt completed without failures.
170- if ! res .stopped && ! t .Failed () {
173+ if ! res .stopped && ! t .Failed () && ! attemptHitOwnTimeout {
171174 return
172175 }
173176
174177 // Wait for pollInterval, or context is canceled or deadline is reached.
175- sleep (awaitCtx , deadline , pollInterval )
178+ sleep (awaitCtx , deadline , cfg . pollInterval )
176179 }
177180}
178181
0 commit comments