Skip to content

Commit

Permalink
test: add unit tests for all executors
Browse files Browse the repository at this point in the history
  • Loading branch information
lvlcn-t committed May 2, 2024
1 parent 2ce1c77 commit 56bac3d
Show file tree
Hide file tree
Showing 5 changed files with 389 additions and 16 deletions.
67 changes: 67 additions & 0 deletions executors/circuitbreaker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package executors

import (
"context"
"errors"
"testing"
"time"
)

func TestCircuitBreaker(t *testing.T) {
tests := []struct {
name string
maxFailures int
resetTimeout time.Duration
effector Effector
wantErr bool
}{
{
name: "circuit open",
maxFailures: 2,
resetTimeout: 10 * time.Millisecond,
effector: func(ctx context.Context) error {
return errors.New("error")
},
wantErr: true,
},
{
name: "circuit closed",
maxFailures: 2,
resetTimeout: 10 * time.Millisecond,
effector: noopEffector,
wantErr: false,
},
{
name: "circuit open then closed",
maxFailures: 2,
resetTimeout: 10 * time.Millisecond,
effector: func(ctx context.Context) error {
return errors.New("error")
},
wantErr: true,
},
{
name: "nil effector",
effector: nil,
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
effector := CircuitBreaker(tt.maxFailures, tt.resetTimeout, tt.effector)

for i := 0; i < tt.maxFailures; i++ {
if err := effector(context.Background()); err != nil {
if !tt.wantErr {
t.Errorf("CircuitBreaker() error = %v, wantErr %v", err, tt.wantErr)
}
}
}

if err := effector(context.Background()); (err != nil) != tt.wantErr {
t.Errorf("CircuitBreaker() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
175 changes: 175 additions & 0 deletions executors/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package executors

import (
"context"
"errors"
"sync/atomic"
"testing"
"time"

"golang.org/x/time/rate"
)

func TestEffector_MultiplePolicies(t *testing.T) {
task := func(ctx context.Context) error {
// This task simulates a condition that can timeout or be rate-limited
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(50 * time.Millisecond):
return errors.New("task failed")
}
}

retrier := Retrier{MaxRetries: 3, Backoff: func(retries int) time.Duration { return 10 * time.Millisecond }}
timeout := 100 * time.Millisecond
rateLimit := rate.Every(200 * time.Millisecond)
maxFailures := 1
resetTimeout := 500 * time.Millisecond

effector := Effector(task).
WithRetry(retrier).
WithTimeout(timeout).
WithRateLimit(rateLimit).
WithCircuitBreaker(maxFailures, resetTimeout)

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()

err := effector.Do(ctx)
if err == nil {
t.Error("Expected an error, but got none")
}

// We cannot predict the exact error, but it should be either a deadline exceeded error or a circuit open error
if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, ErrCircuitOpen) {
t.Errorf("Expected either %q or a %q error, but got %v", context.DeadlineExceeded.Error(), ErrCircuitOpen.Error(), err)
}
}

func TestParallel(t *testing.T) {
tests := []struct {
name string
effectors []Effector
wantErr bool
}{
{
name: "nil effectors",
wantErr: false,
},
{
name: "success",
effectors: []Effector{
func(ctx context.Context) error {
return nil
},
},
wantErr: false,
},
{
name: "one error",
effectors: []Effector{
func(ctx context.Context) error {
return nil
},
func(ctx context.Context) error {
return errors.New("task failed")
},
},
wantErr: true,
},
{
name: "all errors",
effectors: []Effector{
func(ctx context.Context) error {
return errors.New("task failed")
},
func(ctx context.Context) error {
return errors.New("task failed")
},
},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
effector := Parallel(tt.effectors...)

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()

err := effector.Do(ctx)
if (err != nil) != tt.wantErr {
t.Errorf("Parallel() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func TestSequential(t *testing.T) {
var executionOrder int32
tests := []struct {
name string
effectors []Effector
wantErr bool
}{
{
name: "nil effectors",
wantErr: false,
},
{
name: "success",
effectors: []Effector{
func(ctx context.Context) error {
return nil
},
},
wantErr: false,
},
{
name: "one error",
effectors: []Effector{
func(ctx context.Context) error {
atomic.StoreInt32(&executionOrder, 1)
return nil
},
func(ctx context.Context) error {
if atomic.LoadInt32(&executionOrder) != 1 {
t.Error("Expected the first effector to run first")
}
return errors.New("task failed")
},
},
wantErr: true,
},
{
name: "all errors",
effectors: []Effector{
func(ctx context.Context) error {
atomic.StoreInt32(&executionOrder, 1)
return errors.New("task failed")
},
func(ctx context.Context) error {
if atomic.LoadInt32(&executionOrder) != 1 {
t.Error("Expected the first effector to run first")
}
return errors.New("task failed")
},
},
wantErr: true,
},
}

for _, tt := range tests {
executionOrder = 0
t.Run(tt.name, func(t *testing.T) {
effector := Sequential(tt.effectors...)

err := effector.Do()
if (err != nil) != tt.wantErr {
t.Errorf("Sequential() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
57 changes: 57 additions & 0 deletions executors/ratelimit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package executors

import (
"context"
"testing"

"golang.org/x/time/rate"
)

func TestRateLimiter(t *testing.T) {
tests := []struct {
name string
rate rate.Limit
effector Effector
wantErr bool
}{
{
name: "nil effector",
rate: 1,
effector: nil,
wantErr: false,
},
{
name: "success",
rate: 1,
effector: noopEffector,
wantErr: false,
},
{
name: "rate limit with negative rate",
rate: -1,
effector: func(ctx context.Context) error {
return nil
},
wantErr: true,
},
{
name: "rate limit with zero rate",
rate: 0,
effector: func(ctx context.Context) error {
return nil
},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
effector := RateLimiter(tt.rate, tt.effector)

err := effector(context.Background())
if (err != nil) != tt.wantErr {
t.Errorf("RateLimiter() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
26 changes: 10 additions & 16 deletions executors/retry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,10 @@ func TestRetrier_Retry(t *testing.T) {
wantErr: false,
},
{
name: "nil backoff",
retrier: &Retrier{},
effector: func(ctx context.Context) error {
return nil
},
wantErr: false,
name: "nil backoff",
retrier: &Retrier{},
effector: noopEffector,
wantErr: false,
},
{
name: "success",
Expand All @@ -39,18 +37,14 @@ func TestRetrier_Retry(t *testing.T) {
return 1
},
},
effector: func(ctx context.Context) error {
return nil
},
wantErr: false,
effector: noopEffector,
wantErr: false,
},
{
name: "success with default retrier",
retrier: nil,
effector: func(ctx context.Context) error {
return nil
},
wantErr: false,
name: "success with default retrier",
retrier: nil,
effector: noopEffector,
wantErr: false,
},
{
name: "error",
Expand Down
Loading

0 comments on commit 56bac3d

Please sign in to comment.