From e23223ad02ddeafe581df97c7e9462df28d8e104 Mon Sep 17 00:00:00 2001 From: Philipp Hinrichsen Date: Tue, 8 Feb 2022 18:22:41 +0100 Subject: [PATCH] refactor: extract RunContext Executor in JobExecutor (#984) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This splits the executor from the RunContext into its own function called newJobExecutor. We defined an interface called jobInfo which is implemented by the RunContext. This enables better unit testing because only a small interface needs to be mocked. This is a preparation for implementing pre and post actions. Co-authored-by: Björn Brauer Co-authored-by: Marcus Noll Co-authored-by: Jonas Holland Co-authored-by: Robert Kowalski Co-authored-by: Markus Wolf Co-authored-by: Björn Brauer Co-authored-by: Marcus Noll Co-authored-by: Jonas Holland Co-authored-by: Robert Kowalski Co-authored-by: Markus Wolf --- go.sum | 1 + pkg/runner/job_executor.go | 71 +++++++++++++++++ pkg/runner/job_executor_test.go | 135 ++++++++++++++++++++++++++++++++ pkg/runner/run_context.go | 71 ++++++----------- 4 files changed, 233 insertions(+), 45 deletions(-) create mode 100644 pkg/runner/job_executor.go create mode 100644 pkg/runner/job_executor_test.go diff --git a/go.sum b/go.sum index 2fde5de2..67967479 100644 --- a/go.sum +++ b/go.sum @@ -1117,6 +1117,7 @@ github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/pkg/runner/job_executor.go b/pkg/runner/job_executor.go new file mode 100644 index 00000000..c6068ae5 --- /dev/null +++ b/pkg/runner/job_executor.go @@ -0,0 +1,71 @@ +package runner + +import ( + "context" + "fmt" + + "github.com/nektos/act/pkg/common" + "github.com/nektos/act/pkg/model" +) + +type jobInfo interface { + matrix() map[string]interface{} + steps() []*model.Step + startContainer() common.Executor + stopContainer() common.Executor + closeContainer() common.Executor + newStepExecutor(step *model.Step) common.Executor + interpolateOutputs() common.Executor + result(result string) +} + +func newJobExecutor(info jobInfo) common.Executor { + steps := make([]common.Executor, 0) + + steps = append(steps, func(ctx context.Context) error { + if len(info.matrix()) > 0 { + common.Logger(ctx).Infof("\U0001F9EA Matrix: %v", info.matrix()) + } + return nil + }) + + steps = append(steps, info.startContainer()) + + for i, step := range info.steps() { + if step.ID == "" { + step.ID = fmt.Sprintf("%d", i) + } + stepExec := info.newStepExecutor(step) + steps = append(steps, func(ctx context.Context) error { + err := stepExec(ctx) + if err != nil { + common.Logger(ctx).Errorf("%v", err) + common.SetJobError(ctx, err) + } else if ctx.Err() != nil { + common.Logger(ctx).Errorf("%v", ctx.Err()) + common.SetJobError(ctx, ctx.Err()) + } + return nil + }) + } + steps = append(steps, func(ctx context.Context) error { + err := info.stopContainer()(ctx) + if err != nil { + return err + } + + jobError := common.JobError(ctx) + if jobError != nil { + info.result("failure") + } else { + info.result("success") + } + + return nil + }) + + return common.NewPipelineExecutor(steps...).Finally(info.interpolateOutputs()).Finally(func(ctx context.Context) error { + info.closeContainer() + return nil + }) +} diff --git a/pkg/runner/job_executor_test.go b/pkg/runner/job_executor_test.go new file mode 100644 index 00000000..0444aef6 --- /dev/null +++ b/pkg/runner/job_executor_test.go @@ -0,0 +1,135 @@ +package runner + +import ( + "context" + "fmt" + "testing" + + "github.com/nektos/act/pkg/common" + "github.com/nektos/act/pkg/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +type jobInfoMock struct { + mock.Mock +} + +func (jpm *jobInfoMock) matrix() map[string]interface{} { + args := jpm.Called() + return args.Get(0).(map[string]interface{}) +} + +func (jpm *jobInfoMock) steps() []*model.Step { + args := jpm.Called() + + return args.Get(0).([]*model.Step) +} + +func (jpm *jobInfoMock) startContainer() common.Executor { + args := jpm.Called() + + return args.Get(0).(func(context.Context) error) +} + +func (jpm *jobInfoMock) stopContainer() common.Executor { + args := jpm.Called() + + return args.Get(0).(func(context.Context) error) +} + +func (jpm *jobInfoMock) closeContainer() common.Executor { + args := jpm.Called() + + return args.Get(0).(func(context.Context) error) +} + +func (jpm *jobInfoMock) newStepExecutor(step *model.Step) common.Executor { + args := jpm.Called(step) + + return args.Get(0).(func(context.Context) error) +} + +func (jpm *jobInfoMock) interpolateOutputs() common.Executor { + args := jpm.Called() + + return args.Get(0).(func(context.Context) error) +} + +func (jpm *jobInfoMock) result(result string) { + jpm.Called(result) +} + +func TestNewJobExecutor(t *testing.T) { + table := []struct { + name string + steps []*model.Step + result string + hasError bool + }{ + { + "zeroSteps", + []*model.Step{}, + "success", + false, + }, + { + "stepWithoutPrePost", + []*model.Step{{ + ID: "1", + }}, + "success", + false, + }, + { + "stepWithFailure", + []*model.Step{{ + ID: "1", + }}, + "failure", + true, + }, + } + + for _, tt := range table { + t.Run(tt.name, func(t *testing.T) { + ctx := common.WithJobErrorContainer(context.Background()) + jpm := &jobInfoMock{} + + jpm.On("startContainer").Return(func(ctx context.Context) error { + return nil + }) + + jpm.On("steps").Return(tt.steps) + + for _, stepMock := range tt.steps { + jpm.On("newStepExecutor", stepMock).Return(func(ctx context.Context) error { + if tt.hasError { + return fmt.Errorf("error") + } + return nil + }) + } + + jpm.On("interpolateOutputs").Return(func(ctx context.Context) error { + return nil + }) + + jpm.On("matrix").Return(map[string]interface{}{}) + + jpm.On("stopContainer").Return(func(ctx context.Context) error { + return nil + }) + + jpm.On("result", tt.result) + + jpm.On("closeContainer").Return(func(ctx context.Context) error { + return nil + }) + + executor := newJobExecutor(jpm) + err := executor(ctx) + assert.Nil(t, err) + }) + } +} diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index e072b04a..abe07166 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -244,57 +244,38 @@ func (rc *RunContext) interpolateOutputs() common.Executor { } } -// Executor returns a pipeline executor for all the steps in the job -func (rc *RunContext) Executor() common.Executor { - steps := make([]common.Executor, 0) +func (rc *RunContext) startContainer() common.Executor { + return rc.startJobContainer() +} - steps = append(steps, func(ctx context.Context) error { - if len(rc.Matrix) > 0 { - common.Logger(ctx).Infof("\U0001F9EA Matrix: %v", rc.Matrix) - } - return nil - }) +func (rc *RunContext) stopContainer() common.Executor { + return rc.stopJobContainer() +} - steps = append(steps, rc.startJobContainer()) - - for i, step := range rc.Run.Job().Steps { - if step.ID == "" { - step.ID = fmt.Sprintf("%d", i) - } - stepExec := rc.newStepExecutor(step) - steps = append(steps, func(ctx context.Context) error { - err := stepExec(ctx) - if err != nil { - common.Logger(ctx).Errorf("%v", err) - common.SetJobError(ctx, err) - } else if ctx.Err() != nil { - common.Logger(ctx).Errorf("%v", ctx.Err()) - common.SetJobError(ctx, ctx.Err()) - } - return nil - }) - } - steps = append(steps, func(ctx context.Context) error { - err := rc.stopJobContainer()(ctx) - if err != nil { - return err - } - - rc.Run.Job().Result = "success" - jobError := common.JobError(ctx) - if jobError != nil { - rc.Run.Job().Result = "failure" - } - - return nil - }) - - return common.NewPipelineExecutor(steps...).Finally(rc.interpolateOutputs()).Finally(func(ctx context.Context) error { +func (rc *RunContext) closeContainer() common.Executor { + return func(ctx context.Context) error { if rc.JobContainer != nil { return rc.JobContainer.Close()(ctx) } return nil - }).If(rc.isEnabled) + } +} + +func (rc *RunContext) matrix() map[string]interface{} { + return rc.Matrix +} + +func (rc *RunContext) result(result string) { + rc.Run.Job().Result = result +} + +func (rc *RunContext) steps() []*model.Step { + return rc.Run.Job().Steps +} + +// Executor returns a pipeline executor for all the steps in the job +func (rc *RunContext) Executor() common.Executor { + return newJobExecutor(rc).If(rc.isEnabled) } // Executor returns a pipeline executor for all the steps in the job