From 43180bdacf80f147b53b66c8f56c81a971921c6e Mon Sep 17 00:00:00 2001 From: lasith-kg Date: Wed, 3 Jan 2024 00:36:05 +0000 Subject: [PATCH] (feat): Add test coverage for layer.ExponentialBackoffLayerExecutor() --- .gitignore | 3 +- cmd/ebs-bootstrap.go | 2 +- internal/backend/file_test.go | 6 +- internal/layer/layer.go | 35 ++++++---- internal/layer/layer_test.go | 118 ++++++++++++++++++++++++++++++++++ internal/service/mock.go | 36 +++++------ internal/utils/exec.go | 7 +- internal/utils/testing.go | 57 +++++++++++++++- 8 files changed, 223 insertions(+), 41 deletions(-) create mode 100644 internal/layer/layer_test.go diff --git a/.gitignore b/.gitignore index 98b1939..713ba9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ /ebs-bootstrap* -coverage.html -coverage.out \ No newline at end of file +coverage.* \ No newline at end of file diff --git a/cmd/ebs-bootstrap.go b/cmd/ebs-bootstrap.go index 816c764..4fd0183 100644 --- a/cmd/ebs-bootstrap.go +++ b/cmd/ebs-bootstrap.go @@ -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), diff --git a/internal/backend/file_test.go b/internal/backend/file_test.go index 317fa65..06a42c2 100644 --- a/internal/backend/file_test.go +++ b/internal/backend/file_test.go @@ -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{ @@ -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{ @@ -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{ diff --git a/internal/layer/layer.go b/internal/layer/layer.go index dc38577..ccff3d5 100644 --- a/internal/layer/layer.go +++ b/internal/layer/layer.go @@ -10,12 +10,6 @@ import ( "github.com/reecetech/ebs-bootstrap/internal/config" ) -const ( - InitialInterval = 200 * time.Millisecond - Multiplier = 2 - MaxRetries = 3 -) - const ( DisabledWarning = "" ) @@ -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, diff --git a/internal/layer/layer_test.go b/internal/layer/layer_test.go new file mode 100644 index 0000000..e59e8f8 --- /dev/null +++ b/internal/layer/layer_test.go @@ -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 ExponentialBackoffParameters with a custom + // InitialInterval. We do not want to slow down the test suite + // with an excessively long initial interval + 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) + }) + } +} diff --git a/internal/service/mock.go b/internal/service/mock.go index 3d6ddd8..b1a3ae4 100644 --- a/internal/service/mock.go +++ b/internal/service/mock.go @@ -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()") }, } } @@ -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()") }, } } @@ -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()") }, } } @@ -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 @@ -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()") }, } } diff --git a/internal/utils/exec.go b/internal/utils/exec.go index ea024c8..a1c433b 100644 --- a/internal/utils/exec.go +++ b/internal/utils/exec.go @@ -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, } } diff --git a/internal/utils/testing.go b/internal/utils/testing.go index 7e79617..53b2f1c 100644 --- a/internal/utils/testing.go +++ b/internal/utils/testing.go @@ -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) { @@ -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 +}