Skip to content

Commit

Permalink
(feat): Add test coverage for layer.ExponentialBackoffLayerExecutor()
Browse files Browse the repository at this point in the history
  • Loading branch information
lasith-kg committed Jan 3, 2024
1 parent 6fbd4a7 commit 53bf640
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 41 deletions.
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
/ebs-bootstrap*
coverage.html
coverage.out
coverage.*
2 changes: 1 addition & 1 deletion cmd/ebs-bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func main() {
}

// Layers
le := layer.NewExponentialBackoffLayerExecutor(c, dae)
le := layer.NewExponentialBackoffLayerExecutor(c, dae, layer.DefaultExponentialBackoffParameters())
layers := []layer.Layer{
layer.NewFormatDeviceLayer(db),
layer.NewLabelDeviceLayer(db),
Expand Down
6 changes: 3 additions & 3 deletions internal/backend/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ func TestLinuxFileBackendFrom(t *testing.T) {
case "/mnt":
return &model.File{Path: p, Type: model.Directory}, nil
default:
return nil, utils.NotImeplementedError("GetFile()")
return nil, utils.NewNotImeplementedError("GetFile()")
}
},
ExpectedOutput: map[string]*model.File{
Expand Down Expand Up @@ -318,7 +318,7 @@ func TestLinuxFileBackendFrom(t *testing.T) {
counter++
return &model.File{Path: p, Type: model.Directory}, nil
default:
return nil, utils.NotImeplementedError("GetFile()")
return nil, utils.NewNotImeplementedError("GetFile()")
}
},
ExpectedOutput: map[string]*model.File{
Expand Down Expand Up @@ -355,7 +355,7 @@ func TestLinuxFileBackendFrom(t *testing.T) {
case "/mnt":
return &model.File{Path: "/mnt", Type: model.Directory}, nil
default:
return nil, utils.NotImeplementedError("GetFile()")
return nil, utils.NewNotImeplementedError("GetFile()")
}
},
ExpectedOutput: map[string]*model.File{
Expand Down
35 changes: 24 additions & 11 deletions internal/layer/layer.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ import (
"github.com/reecetech/ebs-bootstrap/internal/config"
)

const (
InitialInterval = 200 * time.Millisecond
Multiplier = 2
MaxRetries = 3
)

const (
DisabledWarning = ""
)
Expand All @@ -37,17 +31,36 @@ type ExponentialBackoffLayerExecutor struct {
config *config.Config
}

func NewExponentialBackoffLayerExecutor(c *config.Config, ae action.ActionExecutor) *ExponentialBackoffLayerExecutor {
type ExponentialBackoffParameters struct {
InitialInterval time.Duration
Multiplier uint32
MaxRetries uint32
}

func DefaultExponentialBackoffParameters() *ExponentialBackoffParameters {
return &ExponentialBackoffParameters{
InitialInterval: 200 * time.Millisecond,
Multiplier: 2,
MaxRetries: 3,
}
}

func NewExponentialBackoffLayerExecutor(c *config.Config, ae action.ActionExecutor, ebp *ExponentialBackoffParameters) *ExponentialBackoffLayerExecutor {
// Cast Multiplier and MaxRetries to float64 for use in the backoff calculation
m := float64(ebp.Multiplier)
mr := float64(ebp.MaxRetries)

// Create Backoff
bo := backoff.NewExponentialBackOff()
bo.InitialInterval = InitialInterval
bo.InitialInterval = ebp.InitialInterval
// Disable randomisation of the calculation of the next desired backoff duration
bo.RandomizationFactor = 0
// Set the multiplier for the exponential backoff, using the square root of the provided Multiplier.
bo.Multiplier = math.Sqrt(Multiplier)
bo.Multiplier = math.Sqrt(m)
// Calculate the maximum elapsed time for the backoff strategy based on the provided MaxRetries and InitialInterval.
// This formula calculates the maximum elapsed time as a geometric series sum for a given number of retries and interval.
bo.MaxElapsedTime = time.Duration((math.Pow(Multiplier, MaxRetries)-1)/(Multiplier-1)) * InitialInterval
bo.MaxInterval = InitialInterval * time.Duration(math.Pow(Multiplier, MaxRetries-1))
bo.MaxElapsedTime = time.Duration((math.Pow(m, mr)-1)/(m-1)) * ebp.InitialInterval
bo.MaxInterval = ebp.InitialInterval * time.Duration(math.Pow(m, mr-1))
return &ExponentialBackoffLayerExecutor{
backoff: bo,
actionExecutor: ae,
Expand Down
118 changes: 118 additions & 0 deletions internal/layer/layer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package layer

import (
"fmt"
"testing"
"time"

"github.com/reecetech/ebs-bootstrap/internal/action"
"github.com/reecetech/ebs-bootstrap/internal/config"
"github.com/reecetech/ebs-bootstrap/internal/utils"
)

const (
MaxUint32 = ^uint32(0)
)

type MockLayer struct {
from *utils.MockIncrementError
modify *utils.MockIncrementError
validate *utils.MockIncrementError
}

func (ml *MockLayer) From(c *config.Config) error {
return ml.from.Trigger()
}

func (ml *MockLayer) Modify(c *config.Config) ([]action.Action, error) {
err := ml.modify.Trigger()
if err != nil {
return nil, err
}
return []action.Action{}, nil
}

func (ml *MockLayer) Validate(c *config.Config) error {
return ml.validate.Trigger()
}

func (ml *MockLayer) Warning() string {
return DisabledWarning
}

func TestExponentialBackoffLayerExecutor(t *testing.T) {
mae := action.NewDefaultActionExecutor()
// Lets generate some ExponentialBackoffParameters with a custom
// initial interval. We do not want to slow down the test suite
// with an interval that might be more suitable for production.
debp := DefaultExponentialBackoffParameters()
ebp := &ExponentialBackoffParameters{
InitialInterval: 1 * time.Millisecond,
Multiplier: debp.Multiplier,
MaxRetries: debp.MaxRetries,
}

subtests := []struct {
Name string
From *utils.MockIncrementError
Modify *utils.MockIncrementError
Validate *utils.MockIncrementError
ExpectedError error
}{
{
Name: "Success",
From: utils.NewMockIncrementError("From()", utils.SuccessUntilTrigger, MaxUint32),
Modify: utils.NewMockIncrementError("Modify()", utils.SuccessUntilTrigger, MaxUint32),
Validate: utils.NewMockIncrementError("Validate()", utils.SuccessUntilTrigger, MaxUint32),
ExpectedError: nil,
},
{
Name: "From() - Failure on First Call",
From: utils.NewMockIncrementError("From()", utils.SuccessUntilTrigger, 1),
Modify: utils.NewMockIncrementError("Modify()", utils.SuccessUntilTrigger, MaxUint32),
Validate: utils.NewMockIncrementError("Validate()", utils.SuccessUntilTrigger, MaxUint32),
ExpectedError: fmt.Errorf("🔴 From(): Type=SuccessUntilTrigger, Increment=1, Trigger=1"),
},
{
Name: "From() - Trigger Permant Backoff Failure",
From: utils.NewMockIncrementError("From()", utils.SuccessUntilTrigger, 2),
Modify: utils.NewMockIncrementError("Modify()", utils.SuccessUntilTrigger, MaxUint32),
Validate: utils.NewMockIncrementError("Validate()", utils.SuccessUntilTrigger, MaxUint32),
ExpectedError: fmt.Errorf("🔴 From(): Type=SuccessUntilTrigger, Increment=2, Trigger=2"),
},
{
Name: "Modify() - Failure on First Call",
From: utils.NewMockIncrementError("From()", utils.SuccessUntilTrigger, MaxUint32),
Modify: utils.NewMockIncrementError("Modify()", utils.SuccessUntilTrigger, 1),
Validate: utils.NewMockIncrementError("Validate()", utils.SuccessUntilTrigger, MaxUint32),
ExpectedError: fmt.Errorf("🔴 Modify(): Type=SuccessUntilTrigger, Increment=1, Trigger=1"),
},
// The number of times Validate() would be the initial call (1) plus the number of allowed retries (ebp.MaxRetries)
{
Name: "Validate() - Success Just Before Maximum Retries Reached",
From: utils.NewMockIncrementError("From()", utils.SuccessUntilTrigger, MaxUint32),
Modify: utils.NewMockIncrementError("Modify()", utils.SuccessUntilTrigger, MaxUint32),
Validate: utils.NewMockIncrementError("Validate()", utils.ErrorUntilTrigger, 1+ebp.MaxRetries),
ExpectedError: nil,
},
{
Name: "Validate() - Trigger Maximum Retries",
From: utils.NewMockIncrementError("From()", utils.SuccessUntilTrigger, MaxUint32),
Modify: utils.NewMockIncrementError("Modify()", utils.SuccessUntilTrigger, MaxUint32),
Validate: utils.NewMockIncrementError("Validate()", utils.ErrorUntilTrigger, 2+ebp.MaxRetries),
ExpectedError: fmt.Errorf("🔴 Validate(): Type=ErrorUntilTrigger, Increment=%d, Trigger=%d", 1+ebp.MaxRetries, 2+ebp.MaxRetries),
},
}
for _, subtest := range subtests {
t.Run(subtest.Name, func(t *testing.T) {
ml := &MockLayer{
from: subtest.From,
modify: subtest.Modify,
validate: subtest.Validate,
}
eb := NewExponentialBackoffLayerExecutor(nil, mae, ebp)
err := eb.Execute([]Layer{ml})
utils.CheckError("eb.Execute()", t, subtest.ExpectedError, err)
})
}
}
36 changes: 18 additions & 18 deletions internal/service/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@ type MockDeviceService struct {
func NewMockDeviceService() *MockDeviceService {
return &MockDeviceService{
StubGetSize: func(name string) (uint64, error) {
return 0, utils.NotImeplementedError("GetSize()")
return 0, utils.NewNotImeplementedError("GetSize()")
},
StubGetBlockDevices: func() ([]string, error) {
return nil, utils.NotImeplementedError("GetBlockDevices()")
return nil, utils.NewNotImeplementedError("GetBlockDevices()")
},
StubGetBlockDevice: func(name string) (*model.BlockDevice, error) {
return nil, utils.NotImeplementedError("GetBlockDevice()")
return nil, utils.NewNotImeplementedError("GetBlockDevice()")
},
StubMount: func(source, target string, fs model.FileSystem, options model.MountOptions) error {
return utils.NotImeplementedError("Mount()")
return utils.NewNotImeplementedError("Mount()")
},
StubUmount: func(source, target string) error {
return utils.NotImeplementedError("Umount()")
return utils.NewNotImeplementedError("Umount()")
},
}
}
Expand Down Expand Up @@ -63,16 +63,16 @@ type MockOwnerService struct {
func NewMockOwnerService() *MockOwnerService {
return &MockOwnerService{
StubGetCurrentUser: func() (*model.User, error) {
return nil, utils.NotImeplementedError("GetCurrentUser()")
return nil, utils.NewNotImeplementedError("GetCurrentUser()")
},
StubGetCurrentGroup: func() (*model.Group, error) {
return nil, utils.NotImeplementedError("GetCurrentGroup()")
return nil, utils.NewNotImeplementedError("GetCurrentGroup()")
},
StubGetUser: func(usr string) (*model.User, error) {
return nil, utils.NotImeplementedError("GetUser()")
return nil, utils.NewNotImeplementedError("GetUser()")
},
StubGetGroup: func(grp string) (*model.Group, error) {
return nil, utils.NotImeplementedError("GetGroup()")
return nil, utils.NewNotImeplementedError("GetGroup()")
},
}
}
Expand Down Expand Up @@ -100,7 +100,7 @@ type MockNVMeService struct {
func NewMockNVMeService() *MockNVMeService {
return &MockNVMeService{
StubGetBlockDeviceMapping: func(device string) (string, error) {
return "", utils.NotImeplementedError("GetBlockDeviceMapping()")
return "", utils.NewNotImeplementedError("GetBlockDeviceMapping()")
},
}
}
Expand Down Expand Up @@ -147,19 +147,19 @@ type MockFileSystemService struct {
func NewMockFileSystemService() *MockFileSystemService {
return &MockFileSystemService{
StubGetSize: func(name string) (uint64, error) {
return 0, utils.NotImeplementedError("GetSize()")
return 0, utils.NewNotImeplementedError("GetSize()")
},
StubGetFileSystem: func() model.FileSystem {
return model.Unformatted
},
StubFormat: func(name string) error {
return utils.NotImeplementedError("Format()")
return utils.NewNotImeplementedError("Format()")
},
StubLabel: func(name string, label string) error {
return utils.NotImeplementedError("Label()")
return utils.NewNotImeplementedError("Label()")
},
StubResize: func(name string) error {
return utils.NotImeplementedError("Resize()")
return utils.NewNotImeplementedError("Resize()")
},
StubGetMaximumLabelLength: func() int {
return 0
Expand Down Expand Up @@ -215,16 +215,16 @@ type MockFileService struct {
func NewMockFileService() *MockFileService {
return &MockFileService{
StubGetFile: func(file string) (*model.File, error) {
return nil, utils.NotImeplementedError("GetFile()")
return nil, utils.NewNotImeplementedError("GetFile()")
},
StubCreateDirectory: func(p string) error {
return utils.NotImeplementedError("CreateDirectory()")
return utils.NewNotImeplementedError("CreateDirectory()")
},
StubChangeOwner: func(p string, uid model.UserId, gid model.GroupId) error {
return utils.NotImeplementedError("ChangeOwner()")
return utils.NewNotImeplementedError("ChangeOwner()")
},
StubChangePermissions: func(p string, perms model.FilePermissions) error {
return utils.NotImeplementedError("ChangePermissions()")
return utils.NewNotImeplementedError("ChangePermissions()")
},
}
}
Expand Down
7 changes: 3 additions & 4 deletions internal/utils/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,9 @@ type ExecRunner struct {

func NewExecRunner(binary Binary) *ExecRunner {
return &ExecRunner{
binary: binary,
command: exec.Command,
lookPath: exec.LookPath,
isValidated: false,
binary: binary,
command: exec.Command,
lookPath: exec.LookPath,
}
}

Expand Down
57 changes: 55 additions & 2 deletions internal/utils/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,18 @@ import (
"github.com/ryanuber/go-glob"
)

func NotImeplementedError(id string) error {
return fmt.Errorf("🔴 %s is not implemented", id)
type NotImeplementedError struct {
Id string
}

func NewNotImeplementedError(id string) *NotImeplementedError {
return &NotImeplementedError{
Id: id,
}
}

func (e *NotImeplementedError) Error() string {
return fmt.Sprintf("🔴 %s is not implemented", e.Id)
}

func CheckOutput(id string, t *testing.T, expected interface{}, actual interface{}, opts ...cmp.Option) {
Expand Down Expand Up @@ -64,3 +74,46 @@ func CheckErrorGlob(id string, t *testing.T, pattern error, actual error) {
}
}
}

type MockIncrementErrorType string

const (
ErrorUntilTrigger MockIncrementErrorType = "ErrorUntilTrigger"
SuccessUntilTrigger MockIncrementErrorType = "SuccessUntilTrigger"
)

type MockIncrementError struct {
id string
mockErrorType MockIncrementErrorType
trigger uint32
increment uint32
}

func NewMockIncrementError(id string, errorType MockIncrementErrorType, trigger uint32) *MockIncrementError {
return &MockIncrementError{
id: id,
mockErrorType: errorType,
trigger: trigger,
}
}

func (mie *MockIncrementError) Error() string {
return fmt.Sprintf("🔴 %s: Type=%s, Increment=%d, Trigger=%d", mie.id, mie.mockErrorType, mie.increment, mie.trigger)
}

func (mie *MockIncrementError) Trigger() error {
mie.increment++
switch mie.mockErrorType {
case ErrorUntilTrigger:
if mie.increment < mie.trigger {
return mie
}
case SuccessUntilTrigger:
if mie.increment >= mie.trigger {
return mie
}
default:
return fmt.Errorf("🔴 An unsupported MockLayerErrorType was encountered")
}
return nil
}

0 comments on commit 53bf640

Please sign in to comment.