Skip to content

Commit

Permalink
Add Eventually for reusability and simplicity
Browse files Browse the repository at this point in the history
  • Loading branch information
rentziass committed Mar 22, 2023
1 parent 065f0b6 commit 331f24c
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 51 deletions.
74 changes: 57 additions & 17 deletions eventually.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,49 @@ import (
"time"
)

type Eventually struct {
timeout time.Duration
interval time.Duration
maxAttempts int
}

func New(options ...Option) *Eventually {
e := &Eventually{
timeout: 10 * time.Second,
interval: 100 * time.Millisecond,
maxAttempts: 0,
}

for _, option := range options {
option(e)
}

return e
}

func (e *Eventually) Must(t testing.TB, f func(t testing.TB)) {
t.Helper()

r := e.retryableT(t)
keepTrying(t, r, f, t.Fatalf)
}

func (e *Eventually) Should(t testing.TB, f func(t testing.TB)) {
t.Helper()

r := e.retryableT(t)
keepTrying(t, r, f, t.Errorf)
}

func (e *Eventually) retryableT(t testing.TB) *retryableT {
return &retryableT{
TB: t,
timeout: e.timeout,
interval: e.interval,
maxAttempts: e.maxAttempts,
}
}

type failNowPanic struct{}

type retryableT struct {
Expand Down Expand Up @@ -56,45 +99,42 @@ func (r *retryableT) Fatalf(format string, args ...any) {
r.FailNow()
}

type Option func(*retryableT)
type Option func(*Eventually)

func WithTimeout(timeout time.Duration) Option {
return func(r *retryableT) {
r.timeout = timeout
return func(e *Eventually) {
e.timeout = timeout
}
}

func WithInterval(interval time.Duration) Option {
return func(r *retryableT) {
r.interval = interval
return func(e *Eventually) {
e.interval = interval
}
}

func WithMaxAttempts(attempts int) Option {
return func(r *retryableT) {
r.maxAttempts = attempts
return func(e *Eventually) {
e.maxAttempts = attempts
}
}

func Must(t testing.TB, f func(t testing.TB), options ...Option) {
t.Helper()
keepTrying(t, f, t.Fatalf, options...)

e := New(options...)
e.Must(t, f)
}

func Should(t testing.TB, f func(t testing.TB), options ...Option) {
t.Helper()
keepTrying(t, f, t.Errorf, options...)

e := New(options...)
e.Should(t, f)
}

func keepTrying(t testing.TB, f func(t testing.TB), failf func(format string, args ...any), options ...Option) {
func keepTrying(t testing.TB, retryable *retryableT, f func(t testing.TB), failf func(format string, args ...any)) {
t.Helper()
retryable := &retryableT{
TB: t,
}

for _, option := range options {
option(retryable)
}

start := time.Now()
attempts := 0
Expand Down
182 changes: 148 additions & 34 deletions eventually_test.go
Original file line number Diff line number Diff line change
@@ -1,55 +1,169 @@
package eventually_test

import (
"fmt"
"testing"
"time"

"github.com/rentziass/eventually"
)

type test struct {
*testing.T
logs []string
failed bool
halted bool
}
func TestEventually_Must(t *testing.T) {
t.Run("eventually succeeding", func(t *testing.T) {
tt := &test{T: t}

func (t *test) Fail() {
t.failed = true
}
succeed := false

func (t *test) FailNow() {
t.failed = true
t.halted = true
}
e := eventually.New()
e.Must(tt, func(t testing.TB) {
if !succeed {
t.Fail()
succeed = true
}
})

func (t *test) Fatal(args ...interface{}) {
t.Log(args...)
t.FailNow()
}
if tt.failed || tt.halted {
t.Error("test failed")
}
})

func (t *test) Fatalf(format string, args ...interface{}) {
t.Logf(format, args...)
t.FailNow()
}
t.Run("eventually failing", func(t *testing.T) {
tt := &test{T: t}

func (t *test) Error(args ...interface{}) {
t.Log(args...)
t.Fail()
}
e := eventually.New(
eventually.WithMaxAttempts(3),
eventually.WithInterval(0),
)
e.Must(tt, func(t testing.TB) {
t.Fail()
})

func (t *test) Errorf(format string, args ...interface{}) {
t.Logf(format, args...)
t.Fail()
}
if !tt.failed {
t.Error("test succeeded")
}

func (t *test) Log(args ...any) {
t.logs = append(t.logs, fmt.Sprintln(args...))
if !tt.halted {
t.Error("test did not halt")
}
})

t.Run("logs", func(t *testing.T) {
tt := &test{T: t}

e := eventually.New(eventually.WithMaxAttempts(1))
e.Must(tt, func(t testing.TB) {
t.Log("hello")
t.Log("world")
})

if len(tt.logs) != 2 {
t.Fatalf("logs should contain 2 line, contained %d", len(tt.logs))
}

if tt.logs[0] != "hello\n" {
t.Error("unexpected log")
}

if tt.logs[1] != "world\n" {
t.Error("unexpected log")
}
})
}

func (t *test) Logf(format string, args ...any) {
t.logs = append(t.logs, fmt.Sprintf(format, args...))
func TestEventually_Should(t *testing.T) {
t.Run("eventually succeeding", func(t *testing.T) {
tt := &test{T: t}

succeed := false

e := eventually.New()
e.Should(tt, func(t testing.TB) {
if !succeed {
t.Fail()
succeed = true
}
})

if tt.failed || tt.halted {
t.Error("test failed")
}
})

t.Run("eventually failing", func(t *testing.T) {
tt := &test{T: t}

e := eventually.New(
eventually.WithMaxAttempts(3),
eventually.WithInterval(0),
)
e.Should(tt, func(t testing.TB) {
t.Fail()
})

if !tt.failed {
t.Error("test succeeded")
}

if tt.halted {
t.Error("test halted")
}
})

t.Run("logs", func(t *testing.T) {
tt := &test{T: t}

e := eventually.New(eventually.WithMaxAttempts(1))
e.Should(tt, func(t testing.TB) {
t.Log("hello")
t.Log("world")
})

if len(tt.logs) != 2 {
t.Fatalf("logs should contain 2 line, contained %d", len(tt.logs))
}

if tt.logs[0] != "hello\n" {
t.Error("unexpected log")
}

if tt.logs[1] != "world\n" {
t.Error("unexpected log")
}
})

t.Run("multiple uses", func(t *testing.T) {
tt := &test{T: t}

e := eventually.New(eventually.WithMaxAttempts(3), eventually.WithInterval(0))

succeed := false
e.Should(tt, func(t testing.TB) {
if !succeed {
t.Fail()
t.Log("should")
succeed = true
}
})

e.Must(tt, func(t testing.TB) {
t.Log("must")
})

if tt.failed || tt.halted {
t.Error("test failed")
}

if len(tt.logs) != 2 {
t.Fatalf("logs should contain 2 line, contained %d", len(tt.logs))
}

if tt.logs[0] != "should\n" {
t.Error("unexpected log")
}

if tt.logs[1] != "must\n" {
t.Error("unexpected log")
}
})
}

func TestMust(t *testing.T) {
Expand Down

0 comments on commit 331f24c

Please sign in to comment.