Skip to content

Commit

Permalink
wait: add continual function variant (#81)
Browse files Browse the repository at this point in the history
* wait: add continual function variant

* ci: fixup linter warning

* ci: skip cache

* ci: output format json

* ci: fix extra newline
  • Loading branch information
shoenig authored Dec 17, 2022
1 parent a78f3cd commit a4dcd3e
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 25 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
- uses: golangci/golangci-lint-action@v3
with:
version: v1.50
skip-cache: true
- name: Run Go Test
run: |
make test
1 change: 0 additions & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@ linters:
- bodyclose
- durationcheck
- whitespace

6 changes: 3 additions & 3 deletions must/must_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1499,7 +1499,7 @@ func TestWait_BoolFunc(t *testing.T) {
tc := newCase(t, `expected condition to pass within wait context`)
t.Cleanup(tc.assert)

Wait(tc, wait.On(
Wait(tc, wait.InitialSuccess(
wait.BoolFunc(func() bool { return false }),
wait.Timeout(100*time.Millisecond),
))
Expand All @@ -1509,7 +1509,7 @@ func TestWait_ErrorFunc(t *testing.T) {
tc := newCase(t, `expected condition to pass within wait context`)
t.Cleanup(tc.assert)

Wait(tc, wait.On(
Wait(tc, wait.InitialSuccess(
wait.ErrorFunc(func() error { return errors.New("fail") }),
wait.Timeout(100*time.Millisecond),
))
Expand All @@ -1519,7 +1519,7 @@ func TestWait_TestFunc(t *testing.T) {
tc := newCase(t, `expected condition to pass within wait context`)
t.Cleanup(tc.assert)

Wait(tc, wait.On(
Wait(tc, wait.InitialSuccess(
wait.TestFunc(func() (bool, error) { return false, errors.New("fail") }),
wait.Timeout(100*time.Millisecond),
))
Expand Down
146 changes: 135 additions & 11 deletions wait/wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,49 @@ func Gap(duration time.Duration) Option {
}
}

// BoolFunc will retry f while it returns false, or until a wait constraint
// threshold is exceeded.
// BoolFunc executes f under the thresholds of a Constraint.
func BoolFunc(f func() bool) Option {
return func(c *Constraint) {
c.r = boolFunc(f)
if c.continual {
c.r = boolFuncContinual(f)
} else {
c.r = boolFuncInitial(f)
}
}
}

func boolFuncContinual(f func() bool) runnable {
bg := context.Background()
return func(r *runner) *result {
ctx, cancel := context.WithDeadline(bg, r.c.deadline)
defer cancel()

for {
// make an attempt
if !f() {
return &result{Err: ErrConditionUnsatisfied}
}

// used another attempt
r.attempts++

// reached the desired attempts
if r.attempts >= r.c.iterations {
return &result{Err: nil}
}

// wait for gap or time
select {
case <-ctx.Done():
return &result{Err: nil}
case <-time.After(r.c.gap):
// continue
}
}
}
}

func boolFunc(f func() bool) runnable {
func boolFuncInitial(f func() bool) runnable {
bg := context.Background()
return func(r *runner) *result {
ctx, cancel := context.WithDeadline(bg, r.c.deadline)
Expand Down Expand Up @@ -116,11 +150,46 @@ func boolFunc(f func() bool) runnable {
// constraint threshold is exceeded.
func ErrorFunc(f func() error) Option {
return func(c *Constraint) {
c.r = errorFunc(f)
if c.continual {
c.r = errFuncContinual(f)
} else {
c.r = errFuncInitial(f)
}
}
}

func errorFunc(f func() error) runnable {
func errFuncContinual(f func() error) runnable {
bg := context.Background()
return func(r *runner) *result {
ctx, cancel := context.WithDeadline(bg, r.c.deadline)
defer cancel()

for {
// make an attempt
if err := f(); err != nil {
return &result{Err: err}
}

// used another attempt
r.attempts++

// reached the desired attempts
if r.attempts >= r.c.iterations {
return &result{Err: nil}
}

// wait for gap or time
select {
case <-ctx.Done():
return &result{Err: nil}
case <-time.After(r.c.gap):
// continue
}
}
}
}

func errFuncInitial(f func() error) runnable {
bg := context.Background()
return func(r *runner) *result {
ctx, cancel := context.WithDeadline(bg, r.c.deadline)
Expand Down Expand Up @@ -161,11 +230,47 @@ func errorFunc(f func() error) runnable {
// wrapped into the result.
func TestFunc(f func() (bool, error)) Option {
return func(c *Constraint) {
c.r = testFunc(f)
if c.continual {
c.r = testFuncContinual(f)
} else {
c.r = testFuncInitial(f)
}
}
}

func testFunc(f func() (bool, error)) runnable {
func testFuncContinual(f func() (bool, error)) runnable {
bg := context.Background()
return func(r *runner) *result {
ctx, cancel := context.WithDeadline(bg, r.c.deadline)
defer cancel()

for {
// make an attempt
ok, err := f()
if !ok {
return &result{Err: fmt.Errorf("%v: %w", ErrConditionUnsatisfied, err)}
}

// used another attempt
r.attempts++

// reached the desired attempts
if r.attempts >= r.c.iterations {
return &result{Err: nil}
}

// wait for gap or time
select {
case <-ctx.Done():
return &result{Err: nil}
case <-time.After(r.c.gap):
// continue
}
}
}
}

func testFuncInitial(f func() (bool, error)) runnable {
bg := context.Background()
return func(r *runner) *result {
ctx, cancel := context.WithDeadline(bg, r.c.deadline)
Expand Down Expand Up @@ -206,20 +311,38 @@ func testFunc(f func() (bool, error)) runnable {
}
}

// On creates a new Constraint with configuration set by opts.
// InitialSuccess creates a new Constraint configured by opts that will wait for a
// positive result upon calling Constraint.Run. If the threshold of the Constraint
// is exceeded before reaching a positive result, an error is returned from the
// call to Constraint.Run.
//
// Timeout is used to set a maximum amount of time to wait for success.
// Attempts is used to set a maximum number of attempts to wait for success.
// Gap is used to control the amount of time to wait between retries.
func On(opts ...Option) *Constraint {
//
// One of ErrorFunc, BoolFunc, or TestFunc represents the function that will
// be run under the constraint.
func InitialSuccess(opts ...Option) *Constraint {
c := &Constraint{now: time.Now()}
c.setup(opts...)
return c
}

// ContinualSuccess creates a new Constraint configured by opts that will assert
// a positive result is r
func ContinualSuccess(opts ...Option) *Constraint {
c := &Constraint{now: time.Now(), continual: true}
c.setup(opts...)
return c
}

func (c *Constraint) setup(opts ...Option) {
for _, opt := range append([]Option{
Timeout(defaultTimeout),
Gap(defaultGap),
}, opts...) {
opt(c)
}
return c
}

// A Constraint is something a test assertions can wait on before marking the
Expand All @@ -228,6 +351,7 @@ func On(opts ...Option) *Constraint {
// until the number of attempts is exhausted. The interval between retry attempts
// can be configured with Gap.
type Constraint struct {
continual bool // (initial || continual) success
now time.Time
deadline time.Time
gap time.Duration
Expand Down
Loading

0 comments on commit a4dcd3e

Please sign in to comment.