Skip to content

Commit 2bb3e74

Browse files
KnisterPeterZauberNerdMarcus NollrobertkowalskiPhilipp Hinrichsen
authored
feat: split job steps into its own files/structs (#1004)
* refactor: split step_context into separate files This commit moves functions from the step_context.go file into different files, but does otherwise not change anything. This is done to make it easier to review the changes made to these functions in the next commit, where we introduce a step factory to facilitate better unit testing of steps. Co-authored-by: Marcus Noll <[email protected]> Co-authored-by: Björn Brauer <[email protected]> Co-authored-by: Robert Kowalski <[email protected]> Co-authored-by: Philipp Hinrichsen <[email protected]> Co-authored-by: Jonas Holland <[email protected]> * refactor: introduce step factory and make steps testable With this commit we're introducing the `stepFactory` and interfaces and implementations for each different kind of step (run, docker, local and remote actions). Separating each step kind into its own interface and implementation makes it easier to reason about and to change behaviour of the step. By introducing interfaces we enable better unit testability as now each step implementation, the step factory and the job executor can be tested on their own by mocking out parts that are irrelevant. This commits prepares us for implementing pre/post actions in a later PR. Co-authored-by: Marcus Noll <[email protected]> Co-authored-by: Björn Brauer <[email protected]> Co-authored-by: Robert Kowalski <[email protected]> Co-authored-by: Philipp Hinrichsen <[email protected]> Co-authored-by: Jonas Holland <[email protected]> * fix: run post steps in reverse order * test: add missing asserts for mocks * refactor: use local reference instead of function This may make code more easy to follow. * refactor: correct typo in function name * test: use named structs * test: only expected valid calls There are mocks which are only called on certain conditions. * refactor: use step-model to get step name Using the step-logger we have to get the logger name from the step model. * test: only mock stopContainer if required Co-authored-by: Björn Brauer <[email protected]> Co-authored-by: Marcus Noll <[email protected]> Co-authored-by: Robert Kowalski <[email protected]> Co-authored-by: Philipp Hinrichsen <[email protected]> Co-authored-by: Jonas Holland <[email protected]> Co-authored-by: Casey Lee <[email protected]> Co-authored-by: Christopher Homberger <[email protected]>
1 parent 5d7027d commit 2bb3e74

22 files changed

+2239
-1001
lines changed

pkg/runner/action.go

+382-4
Large diffs are not rendered by default.

pkg/runner/action_test.go

+83-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package runner
22

33
import (
4+
"context"
45
"io"
56
"io/fs"
67
"strings"
@@ -121,13 +122,92 @@ runs:
121122
return nil
122123
}
123124

124-
closerMock.On("Close")
125+
if tt.filename != "" {
126+
closerMock.On("Close")
127+
}
125128

126-
sc := &StepContext{}
127-
action, err := sc.readAction(tt.step, "actionDir", "actionPath", readFile, writeFile)
129+
action, err := readActionImpl(tt.step, "actionDir", "actionPath", readFile, writeFile)
128130

129131
assert.Nil(t, err)
130132
assert.Equal(t, tt.expected, action)
133+
134+
closerMock.AssertExpectations(t)
135+
})
136+
}
137+
}
138+
139+
type exprEvalMock struct {
140+
ExpressionEvaluator
141+
mock.Mock
142+
}
143+
144+
func (e *exprEvalMock) Interpolate(expr string) string {
145+
args := e.Called(expr)
146+
return args.String(0)
147+
}
148+
149+
func TestActionRunner(t *testing.T) {
150+
table := []struct {
151+
name string
152+
step actionStep
153+
}{
154+
{
155+
name: "Test",
156+
step: &stepActionRemote{
157+
Step: &model.Step{
158+
Uses: "repo@ref",
159+
},
160+
RunContext: &RunContext{
161+
ActionRepository: "repo",
162+
ActionPath: "path",
163+
ActionRef: "ref",
164+
Config: &Config{},
165+
Run: &model.Run{
166+
JobID: "job",
167+
Workflow: &model.Workflow{
168+
Jobs: map[string]*model.Job{
169+
"job": {
170+
Name: "job",
171+
},
172+
},
173+
},
174+
},
175+
},
176+
action: &model.Action{
177+
Inputs: map[string]model.Input{
178+
"key": {
179+
Default: "default value",
180+
},
181+
},
182+
Runs: model.ActionRuns{
183+
Using: "node16",
184+
},
185+
},
186+
env: map[string]string{},
187+
},
188+
},
189+
}
190+
191+
for _, tt := range table {
192+
t.Run(tt.name, func(t *testing.T) {
193+
ctx := context.Background()
194+
195+
cm := &containerMock{}
196+
cm.On("CopyDir", "/var/run/act/actions/dir/", "dir/", false).Return(func(ctx context.Context) error { return nil })
197+
cm.On("Exec", []string{"node", "/var/run/act/actions/dir/path"}, map[string]string{"INPUT_KEY": "default value"}, "", "").Return(func(ctx context.Context) error { return nil })
198+
tt.step.getRunContext().JobContainer = cm
199+
200+
ee := &exprEvalMock{}
201+
ee.On("Interpolate", "default value").Return("default value")
202+
tt.step.getRunContext().ExprEval = ee
203+
204+
_, localAction := tt.step.(*stepActionRemote)
205+
206+
err := runActionImpl(tt.step, "dir", "path", "repo", "ref", localAction)(ctx)
207+
208+
assert.Nil(t, err)
209+
ee.AssertExpectations(t)
210+
cm.AssertExpectations(t)
131211
})
132212
}
133213
}

pkg/runner/container_mock_test.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package runner
2+
3+
import (
4+
"context"
5+
6+
"github.com/nektos/act/pkg/common"
7+
"github.com/nektos/act/pkg/container"
8+
"github.com/stretchr/testify/mock"
9+
)
10+
11+
type containerMock struct {
12+
mock.Mock
13+
container.Container
14+
}
15+
16+
func (cm *containerMock) Create(capAdd []string, capDrop []string) common.Executor {
17+
args := cm.Called(capAdd, capDrop)
18+
return args.Get(0).(func(context.Context) error)
19+
}
20+
21+
func (cm *containerMock) Pull(forcePull bool) common.Executor {
22+
args := cm.Called(forcePull)
23+
return args.Get(0).(func(context.Context) error)
24+
}
25+
26+
func (cm *containerMock) Start(attach bool) common.Executor {
27+
args := cm.Called(attach)
28+
return args.Get(0).(func(context.Context) error)
29+
}
30+
31+
func (cm *containerMock) Remove() common.Executor {
32+
args := cm.Called()
33+
return args.Get(0).(func(context.Context) error)
34+
}
35+
36+
func (cm *containerMock) Close() common.Executor {
37+
args := cm.Called()
38+
return args.Get(0).(func(context.Context) error)
39+
}
40+
41+
func (cm *containerMock) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor {
42+
args := cm.Called(srcPath, env)
43+
return args.Get(0).(func(context.Context) error)
44+
}
45+
46+
func (cm *containerMock) UpdateFromImageEnv(env *map[string]string) common.Executor {
47+
args := cm.Called(env)
48+
return args.Get(0).(func(context.Context) error)
49+
}
50+
51+
func (cm *containerMock) UpdateFromPath(env *map[string]string) common.Executor {
52+
args := cm.Called(env)
53+
return args.Get(0).(func(context.Context) error)
54+
}
55+
56+
func (cm *containerMock) Copy(destPath string, files ...*container.FileEntry) common.Executor {
57+
args := cm.Called(destPath, files)
58+
return args.Get(0).(func(context.Context) error)
59+
}
60+
61+
func (cm *containerMock) CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor {
62+
args := cm.Called(destPath, srcPath, useGitIgnore)
63+
return args.Get(0).(func(context.Context) error)
64+
}
65+
func (cm *containerMock) Exec(command []string, env map[string]string, user, workdir string) common.Executor {
66+
args := cm.Called(command, env, user, workdir)
67+
return args.Get(0).(func(context.Context) error)
68+
}

pkg/runner/expression.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,7 @@ func (rc *RunContext) NewExpressionEvaluator() ExpressionEvaluator {
7070
}
7171

7272
// NewExpressionEvaluator creates a new evaluator
73-
func (sc *StepContext) NewExpressionEvaluator() ExpressionEvaluator {
74-
rc := sc.RunContext
73+
func (rc *RunContext) NewStepExpressionEvaluator(step step) ExpressionEvaluator {
7574
// todo: cleanup EvaluationEnvironment creation
7675
job := rc.Run.Job()
7776
strategy := make(map[string]interface{})
@@ -97,7 +96,7 @@ func (sc *StepContext) NewExpressionEvaluator() ExpressionEvaluator {
9796

9897
ee := &exprparser.EvaluationEnvironment{
9998
Github: rc.getGithubContext(),
100-
Env: rc.GetEnv(),
99+
Env: *step.getEnv(),
101100
Job: rc.getJobContext(),
102101
Steps: rc.getStepsContext(),
103102
Runner: map[string]interface{}{

pkg/runner/expression_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,13 @@ func TestEvaluateRunContext(t *testing.T) {
147147
}
148148
}
149149

150-
func TestEvaluateStepContext(t *testing.T) {
150+
func TestEvaluateStep(t *testing.T) {
151151
rc := createRunContext(t)
152-
153-
sc := &StepContext{
152+
step := &stepRun{
154153
RunContext: rc,
155154
}
156-
ee := sc.NewExpressionEvaluator()
155+
156+
ee := rc.NewStepExpressionEvaluator(step)
157157

158158
tables := []struct {
159159
in string

pkg/runner/job_executor.go

+33-10
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ type jobInfo interface {
1414
startContainer() common.Executor
1515
stopContainer() common.Executor
1616
closeContainer() common.Executor
17-
newStepExecutor(step *model.Step) common.Executor
1817
interpolateOutputs() common.Executor
1918
result(result string)
2019
}
2120

22-
func newJobExecutor(info jobInfo) common.Executor {
21+
func newJobExecutor(info jobInfo, sf stepFactory, rc *RunContext) common.Executor {
2322
steps := make([]common.Executor, 0)
23+
preSteps := make([]common.Executor, 0)
24+
postSteps := make([]common.Executor, 0)
2425

2526
steps = append(steps, func(ctx context.Context) error {
2627
if len(info.matrix()) > 0 {
@@ -29,15 +30,30 @@ func newJobExecutor(info jobInfo) common.Executor {
2930
return nil
3031
})
3132

32-
steps = append(steps, info.startContainer())
33+
infoSteps := info.steps()
3334

34-
for i, step := range info.steps() {
35-
if step.ID == "" {
36-
step.ID = fmt.Sprintf("%d", i)
35+
if len(infoSteps) == 0 {
36+
return common.NewDebugExecutor("No steps found")
37+
}
38+
39+
preSteps = append(preSteps, info.startContainer())
40+
41+
for i, stepModel := range infoSteps {
42+
if stepModel.ID == "" {
43+
stepModel.ID = fmt.Sprintf("%d", i)
44+
}
45+
46+
step, err := sf.newStep(stepModel, rc)
47+
48+
if err != nil {
49+
return common.NewErrorExecutor(err)
3750
}
38-
stepExec := info.newStepExecutor(step)
51+
52+
preSteps = append(preSteps, step.pre())
53+
54+
stepExec := step.main()
3955
steps = append(steps, func(ctx context.Context) error {
40-
stepName := step.String()
56+
stepName := stepModel.String()
4157
return (func(ctx context.Context) error {
4258
err := stepExec(ctx)
4359
if err != nil {
@@ -50,9 +66,11 @@ func newJobExecutor(info jobInfo) common.Executor {
5066
return nil
5167
})(withStepLogger(ctx, stepName))
5268
})
69+
70+
postSteps = append([]common.Executor{step.post()}, postSteps...)
5371
}
5472

55-
steps = append(steps, func(ctx context.Context) error {
73+
postSteps = append(postSteps, func(ctx context.Context) error {
5674
jobError := common.JobError(ctx)
5775
if jobError != nil {
5876
info.result("failure")
@@ -67,5 +85,10 @@ func newJobExecutor(info jobInfo) common.Executor {
6785
return nil
6886
})
6987

70-
return common.NewPipelineExecutor(steps...).Finally(info.interpolateOutputs()).Finally(info.closeContainer())
88+
pipeline := make([]common.Executor, 0)
89+
pipeline = append(pipeline, preSteps...)
90+
pipeline = append(pipeline, steps...)
91+
pipeline = append(pipeline, postSteps...)
92+
93+
return common.NewPipelineExecutor(pipeline...).Finally(info.interpolateOutputs()).Finally(info.closeContainer())
7194
}

0 commit comments

Comments
 (0)