package runner import ( "context" "fmt" "io" "testing" "github.com/nektos/act/pkg/common" "github.com/nektos/act/pkg/container" "github.com/nektos/act/pkg/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) func TestJobExecutor(t *testing.T) { tables := []TestJobFileInfo{ {workdir, "uses-and-run-in-one-step", "push", "Invalid run/uses syntax for job:test step:Test", platforms}, {workdir, "uses-github-empty", "push", "Expected format {org}/{repo}[/path]@ref", platforms}, {workdir, "uses-github-noref", "push", "Expected format {org}/{repo}[/path]@ref", platforms}, {workdir, "uses-github-root", "push", "", platforms}, {workdir, "uses-github-path", "push", "", platforms}, {workdir, "uses-docker-url", "push", "", platforms}, {workdir, "uses-github-full-sha", "push", "", platforms}, {workdir, "uses-github-short-sha", "push", "Unable to resolve action `actions/hello-world-docker-action@b136eb8`, the provided ref `b136eb8` is the shortened version of a commit SHA, which is not supported. Please use the full commit SHA `b136eb8894c5cb1dd5807da824be97ccdf9b5423` instead", platforms}, {workdir, "job-nil-step", "push", "invalid Step 0: missing run or uses key", platforms}, } // These tests are sufficient to only check syntax. ctx := common.WithDryrun(context.Background(), true) for _, table := range tables { t.Run(table.workflowPath, func(t *testing.T) { table.runTest(ctx, t, &Config{}) }) } } type jobInfoMock struct { mock.Mock } func (jim *jobInfoMock) matrix() map[string]interface{} { args := jim.Called() return args.Get(0).(map[string]interface{}) } func (jim *jobInfoMock) steps() []*model.Step { args := jim.Called() return args.Get(0).([]*model.Step) } func (jim *jobInfoMock) startContainer() common.Executor { args := jim.Called() return args.Get(0).(func(context.Context) error) } func (jim *jobInfoMock) stopContainer() common.Executor { args := jim.Called() return args.Get(0).(func(context.Context) error) } func (jim *jobInfoMock) closeContainer() common.Executor { args := jim.Called() return args.Get(0).(func(context.Context) error) } func (jim *jobInfoMock) interpolateOutputs() common.Executor { args := jim.Called() return args.Get(0).(func(context.Context) error) } func (jim *jobInfoMock) result(result string) { jim.Called(result) } type jobContainerMock struct { container.Container container.LinuxContainerEnvironmentExtensions } func (jcm *jobContainerMock) ReplaceLogWriter(stdout, stderr io.Writer) (io.Writer, io.Writer) { return nil, nil } type stepFactoryMock struct { mock.Mock } func (sfm *stepFactoryMock) newStep(model *model.Step, rc *RunContext) (step, error) { args := sfm.Called(model, rc) return args.Get(0).(step), args.Error(1) } func TestNewJobExecutor(t *testing.T) { table := []struct { name string steps []*model.Step preSteps []bool postSteps []bool executedSteps []string result string hasError bool }{ { name: "zeroSteps", steps: []*model.Step{}, preSteps: []bool{}, postSteps: []bool{}, executedSteps: []string{}, result: "success", hasError: false, }, { name: "stepWithoutPrePost", steps: []*model.Step{{ ID: "1", }}, preSteps: []bool{false}, postSteps: []bool{false}, executedSteps: []string{ "startContainer", "step1", "stopContainer", "interpolateOutputs", "closeContainer", }, result: "success", hasError: false, }, { name: "stepWithFailure", steps: []*model.Step{{ ID: "1", }}, preSteps: []bool{false}, postSteps: []bool{false}, executedSteps: []string{ "startContainer", "step1", "interpolateOutputs", "closeContainer", }, result: "failure", hasError: true, }, { name: "stepWithPre", steps: []*model.Step{{ ID: "1", }}, preSteps: []bool{true}, postSteps: []bool{false}, executedSteps: []string{ "startContainer", "pre1", "step1", "stopContainer", "interpolateOutputs", "closeContainer", }, result: "success", hasError: false, }, { name: "stepWithPost", steps: []*model.Step{{ ID: "1", }}, preSteps: []bool{false}, postSteps: []bool{true}, executedSteps: []string{ "startContainer", "step1", "post1", "stopContainer", "interpolateOutputs", "closeContainer", }, result: "success", hasError: false, }, { name: "stepWithPreAndPost", steps: []*model.Step{{ ID: "1", }}, preSteps: []bool{true}, postSteps: []bool{true}, executedSteps: []string{ "startContainer", "pre1", "step1", "post1", "stopContainer", "interpolateOutputs", "closeContainer", }, result: "success", hasError: false, }, { name: "stepsWithPreAndPost", steps: []*model.Step{{ ID: "1", }, { ID: "2", }, { ID: "3", }}, preSteps: []bool{true, false, true}, postSteps: []bool{false, true, true}, executedSteps: []string{ "startContainer", "pre1", "pre3", "step1", "step2", "step3", "post3", "post2", "stopContainer", "interpolateOutputs", "closeContainer", }, result: "success", hasError: false, }, } contains := func(needle string, haystack []string) bool { for _, item := range haystack { if item == needle { return true } } return false } for _, tt := range table { t.Run(tt.name, func(t *testing.T) { fmt.Printf("::group::%s\n", tt.name) ctx := common.WithJobErrorContainer(context.Background()) jim := &jobInfoMock{} sfm := &stepFactoryMock{} rc := &RunContext{ JobContainer: &jobContainerMock{}, Run: &model.Run{ JobID: "test", Workflow: &model.Workflow{ Jobs: map[string]*model.Job{ "test": {}, }, }, }, Config: &Config{}, } rc.ExprEval = rc.NewExpressionEvaluator(ctx) executorOrder := make([]string, 0) jim.On("steps").Return(tt.steps) if len(tt.steps) > 0 { jim.On("startContainer").Return(func(ctx context.Context) error { executorOrder = append(executorOrder, "startContainer") return nil }) } for i, stepModel := range tt.steps { i := i stepModel := stepModel sm := &stepMock{} sfm.On("newStep", stepModel, rc).Return(sm, nil) sm.On("pre").Return(func(ctx context.Context) error { if tt.preSteps[i] { executorOrder = append(executorOrder, "pre"+stepModel.ID) } return nil }) sm.On("main").Return(func(ctx context.Context) error { executorOrder = append(executorOrder, "step"+stepModel.ID) if tt.hasError { return fmt.Errorf("error") } return nil }) sm.On("post").Return(func(ctx context.Context) error { if tt.postSteps[i] { executorOrder = append(executorOrder, "post"+stepModel.ID) } return nil }) defer sm.AssertExpectations(t) } if len(tt.steps) > 0 { jim.On("matrix").Return(map[string]interface{}{}) jim.On("interpolateOutputs").Return(func(ctx context.Context) error { executorOrder = append(executorOrder, "interpolateOutputs") return nil }) if contains("stopContainer", tt.executedSteps) { jim.On("stopContainer").Return(func(ctx context.Context) error { executorOrder = append(executorOrder, "stopContainer") return nil }) } jim.On("result", tt.result) jim.On("closeContainer").Return(func(ctx context.Context) error { executorOrder = append(executorOrder, "closeContainer") return nil }) } executor := newJobExecutor(jim, sfm, rc) err := executor(ctx) assert.Nil(t, err) assert.Equal(t, tt.executedSteps, executorOrder) jim.AssertExpectations(t) sfm.AssertExpectations(t) fmt.Println("::endgroup::") }) } }