Change:
Calling wait.PollUntilContextTimeout with immediate=true does not invoke the passed condition function if context has already been cancelled. wait.PollUntilContextTimeout with immediate=true should invoke the passed condition function at least once even if context has already been cancelled as wait.PollUntilContextCancel documentation says: Note that wait.PollUntilContextTimeout wraps wait.PollUntilContextCancel, so the above should be applied to wait.PollUntilContextTimeout as well. This program panics because context ctx.Err is checked before invoking the passed function. The deprecated wait.PollImmediateWithContext does not have this kind of issue. If wait.PollUntilContextTimeout is the replacement of wait.PollImmediateWithContext, it should exhibit the same behavior. k8s v1.27.2 <details</details<details</details<details</details<details</details<details</details<details</details
/sig api-machinery
TestloopConditionUntilContextsemantic also has an issue that immediate and sliding are not tested as expected. immediate and sliding are loop variable, but they are not used in the test. The ones that are used are in testcase struct which are always false.
/assign
ok, it seems this would do it
/assign
/triage accepted

Discussion:
// the provided timer until the provided context is cancelled, the condition returns // true, or the condition returns an error. If sliding is true, the period is computed // after condition runs. If it is false then period includes the runtime for condition. <del> // If immediate is false the first delay happens before any call to condition. The // returned error is the error returned by the last condition or the context error if // the context was terminated. </del> <ins> // If immediate is false the first delay happens before any call to condition, if // immediate is true the condition will be invoked before waiting and guarantees that // the condition is invoked at least once, regardless of whether the context has been // cancelled. The returned error is the error returned by the last condition or the // context error if the context was terminated. </ins> // // This is the common loop construct for all polling in the wait package. func loopConditionUntilContext(ctx context.Context, t Timer, immediate, sliding bool, condition ConditionWithContextFunc) error {

var timeCh <-chan time.Time doneCh := ctx.Done() <ins> // if immediate is true the condition is // guaranteed to be executed at least once, </ins> // if we haven't requested immediate execution, delay once <del> if !immediate { </del> <ins> if immediate { if ok, err := func() (bool, error) { defer runtime.HandleCrash() return condition(ctx) }(); err != nil || ok { return err } } else { </ins> timeCh = t.C() select { case <-doneCh:

errExpected:      context.Canceled, }, { <ins> name:    "context already canceled condition success and immediate 1 attempt expected", context: cancelledContext, callback: func(_ int) (bool, error) { return true, nil }, immediate:        true, attemptsExpected: 1, }, { name:    "context already canceled condition fail and immediate 1 attempt expected", context: cancelledContext, callback: func(_ int) (bool, error) { return false, conditionErr }, immediate:        true, attemptsExpected: 1, errExpected:      conditionErr, }, { name:             "context already canceled and immediate 1 attempt expected", context:          cancelledContext, callback:         defaultCallback, immediate:        true, attemptsExpected: 1, errExpected:      context.Canceled, }, { </ins> name:               "context cancelled after 5 attempts", context:            defaultContext, callback:           defaultCallback,

errExpected:        context.Canceled, }, { <ins> name:               "context cancelled and immediate after 5 attempts", context:            defaultContext, callback:           defaultCallback, immediate:          true, cancelContextAfter: 5, attemptsExpected:   5, errExpected:        context.Canceled, }, { name:             "context at deadline and immediate 1 attempt expected", context:          deadlinedContext, callback:         defaultCallback, immediate:        true, attemptsExpected: 1, errExpected:      context.DeadlineExceeded, }, { </ins> name:             "context at deadline no attempts expected", context:          deadlinedContext, callback:         defaultCallback,