Skip to content

Commit ffba61a

Browse files
committed
add mock runner and test jobs with needs
1 parent 2d7e6e9 commit ffba61a

File tree

2 files changed

+329
-0
lines changed

2 files changed

+329
-0
lines changed

tests/integration/actions_job_test.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package integration
5+
6+
import (
7+
"encoding/base64"
8+
"fmt"
9+
"net/http"
10+
"net/url"
11+
"testing"
12+
"time"
13+
14+
actions_model "code.gitea.io/gitea/models/actions"
15+
auth_model "code.gitea.io/gitea/models/auth"
16+
"code.gitea.io/gitea/models/unittest"
17+
user_model "code.gitea.io/gitea/models/user"
18+
api "code.gitea.io/gitea/modules/structs"
19+
20+
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
21+
22+
"github.com/stretchr/testify/assert"
23+
)
24+
25+
func TestJobWithNeeds(t *testing.T) {
26+
testCases := []struct {
27+
treePath string
28+
fileContent string
29+
execPolicies map[string]*taskExecPolicy
30+
expectedStatuses map[string]string
31+
}{
32+
{
33+
treePath: ".gitea/workflows/job-with-needs.yml",
34+
fileContent: `name: test
35+
on: push
36+
jobs:
37+
job1:
38+
runs-on: ubuntu-latest
39+
steps:
40+
- run: echo job1
41+
job2:
42+
runs-on: ubuntu-latest
43+
needs: [job1]
44+
steps:
45+
- run: echo job2
46+
`,
47+
execPolicies: map[string]*taskExecPolicy{
48+
"job1": {
49+
result: runnerv1.Result_RESULT_SUCCESS,
50+
},
51+
"job2": {
52+
result: runnerv1.Result_RESULT_SUCCESS,
53+
},
54+
},
55+
expectedStatuses: map[string]string{
56+
"job1": actions_model.StatusSuccess.String(),
57+
"job2": actions_model.StatusSuccess.String(),
58+
},
59+
},
60+
{
61+
treePath: ".gitea/workflows/job-with-needs-fail.yml",
62+
fileContent: `name: test
63+
on: push
64+
jobs:
65+
job1:
66+
runs-on: ubuntu-latest
67+
steps:
68+
- run: echo job1
69+
job2:
70+
runs-on: ubuntu-latest
71+
needs: [job1]
72+
steps:
73+
- run: echo job2
74+
`,
75+
execPolicies: map[string]*taskExecPolicy{
76+
"job1": {
77+
result: runnerv1.Result_RESULT_FAILURE,
78+
},
79+
},
80+
expectedStatuses: map[string]string{
81+
"job1": actions_model.StatusFailure.String(),
82+
"job2": actions_model.StatusSkipped.String(),
83+
},
84+
},
85+
}
86+
onGiteaRun(t, func(t *testing.T, u *url.URL) {
87+
runner := newMockRunner(t)
88+
runner.registerAsGlobalRunner(t, "mock-runner", []string{"ubuntu-latest"})
89+
90+
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
91+
session := loginUser(t, user2.Name)
92+
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
93+
94+
// create the repo
95+
req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
96+
Name: "actions-jobs-with-needs",
97+
Description: "test jobs with needs",
98+
Private: false,
99+
Readme: "Default",
100+
AutoInit: true,
101+
DefaultBranch: "main",
102+
}).AddTokenAuth(token)
103+
resp := MakeRequest(t, req, http.StatusCreated)
104+
var apiRepo api.Repository
105+
DecodeJSON(t, resp, &apiRepo)
106+
107+
for _, tc := range testCases {
108+
// create the workflow file
109+
createFileOptions := &api.CreateFileOptions{
110+
FileOptions: api.FileOptions{
111+
BranchName: "main",
112+
Message: fmt.Sprintf("create %s", tc.treePath),
113+
Author: api.Identity{
114+
Name: user2.Name,
115+
Email: user2.Email,
116+
},
117+
Committer: api.Identity{
118+
Name: user2.Name,
119+
Email: user2.Email,
120+
},
121+
Dates: api.CommitDateOptions{
122+
Author: time.Now(),
123+
Committer: time.Now(),
124+
},
125+
},
126+
ContentBase64: base64.StdEncoding.EncodeToString([]byte(tc.fileContent)),
127+
}
128+
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, apiRepo.Name, tc.treePath), createFileOptions).
129+
AddTokenAuth(token)
130+
var fileResponse api.FileResponse
131+
resp = MakeRequest(t, req, http.StatusCreated)
132+
DecodeJSON(t, resp, &fileResponse)
133+
134+
// fetch and execute task
135+
for i := 0; i < len(tc.execPolicies); i++ {
136+
task := runner.fetchTask(t)
137+
assert.NotNil(t, task)
138+
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
139+
policy := tc.execPolicies[jobName]
140+
assert.NotNil(t, policy)
141+
runner.execTask(t, task, policy)
142+
}
143+
144+
// check result
145+
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", user2.Name, apiRepo.Name)).
146+
AddTokenAuth(token)
147+
resp = MakeRequest(t, req, http.StatusOK)
148+
var actionTaskRespAfter api.ActionTaskResponse
149+
DecodeJSON(t, resp, &actionTaskRespAfter)
150+
for _, apiTask := range actionTaskRespAfter.Entries {
151+
if apiTask.HeadSHA != fileResponse.Commit.SHA {
152+
continue
153+
}
154+
status := apiTask.Status
155+
assert.Equal(t, status, tc.expectedStatuses[apiTask.Name])
156+
}
157+
}
158+
})
159+
}
160+
161+
// getTaskJobNameByTaskID get the job name of the task by task ID
162+
// there is currently not an API for querying a task by ID so we have to list all the tasks
163+
func getTaskJobNameByTaskID(t *testing.T, authToken, repoOwner, repoName string, taskID int64) string {
164+
t.Helper()
165+
// TODO: we may need to query several pages
166+
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", repoOwner, repoName)).
167+
AddTokenAuth(authToken)
168+
resp := MakeRequest(t, req, http.StatusOK)
169+
var taskRespBefore api.ActionTaskResponse
170+
DecodeJSON(t, resp, &taskRespBefore)
171+
for _, apiTask := range taskRespBefore.Entries {
172+
if apiTask.ID == taskID {
173+
return apiTask.Name
174+
}
175+
}
176+
return ""
177+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package integration
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"net/http"
10+
"reflect"
11+
"testing"
12+
13+
auth_model "code.gitea.io/gitea/models/auth"
14+
"code.gitea.io/gitea/modules/setting"
15+
16+
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
17+
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
18+
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
19+
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
20+
21+
"connectrpc.com/connect"
22+
"github.com/stretchr/testify/assert"
23+
"google.golang.org/protobuf/types/known/timestamppb"
24+
)
25+
26+
type mockRunner struct {
27+
client *mockRunnerClient
28+
29+
id int64
30+
uuid string
31+
name string
32+
token string
33+
}
34+
35+
type mockRunnerClient struct {
36+
pingServiceClient pingv1connect.PingServiceClient
37+
runnerServiceClient runnerv1connect.RunnerServiceClient
38+
}
39+
40+
func newMockRunner(t testing.TB) *mockRunner {
41+
t.Helper()
42+
client := newMockRunnerClient("", "")
43+
return &mockRunner{client: client}
44+
}
45+
46+
func newMockRunnerClient(uuid, token string) *mockRunnerClient {
47+
baseURL := fmt.Sprintf("%sapi/actions", setting.AppURL)
48+
49+
opt := connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
50+
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
51+
if uuid != "" {
52+
req.Header().Set("x-runner-uuid", uuid)
53+
}
54+
if token != "" {
55+
req.Header().Set("x-runner-token", token)
56+
}
57+
return next(ctx, req)
58+
}
59+
}))
60+
61+
client := &mockRunnerClient{
62+
pingServiceClient: pingv1connect.NewPingServiceClient(http.DefaultClient, baseURL, opt),
63+
runnerServiceClient: runnerv1connect.NewRunnerServiceClient(http.DefaultClient, baseURL, opt),
64+
}
65+
66+
return client
67+
}
68+
69+
func (r *mockRunner) doPing(t *testing.T) {
70+
resp, err := r.client.pingServiceClient.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{
71+
Data: "mock-runner",
72+
}))
73+
assert.NoError(t, err)
74+
assert.Equal(t, "Hello, mock-runner!", resp.Msg.Data)
75+
}
76+
77+
func (r *mockRunner) doRegister(t *testing.T, name, token string, labels []string) {
78+
r.doPing(t)
79+
resp, err := r.client.runnerServiceClient.Register(context.Background(), connect.NewRequest(&runnerv1.RegisterRequest{
80+
Name: name,
81+
Token: token,
82+
Version: "mock-runner-version",
83+
Labels: labels,
84+
}))
85+
assert.NoError(t, err)
86+
r.id = resp.Msg.Runner.Id
87+
r.uuid = resp.Msg.Runner.Uuid
88+
r.name = resp.Msg.Runner.Name
89+
r.token = resp.Msg.Runner.Token
90+
r.client = newMockRunnerClient(r.uuid, r.token)
91+
}
92+
93+
func (r *mockRunner) registerAsGlobalRunner(t *testing.T, name string, labels []string) {
94+
session := loginUser(t, "user1")
95+
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteAdmin)
96+
req := NewRequest(t, "GET", "/api/v1/admin/runners/registration-token").AddTokenAuth(token)
97+
resp := MakeRequest(t, req, http.StatusOK)
98+
var registrationToken struct {
99+
Token string `json:"token"`
100+
}
101+
DecodeJSON(t, resp, &registrationToken)
102+
r.doRegister(t, name, registrationToken.Token, labels)
103+
}
104+
105+
func (r *mockRunner) fetchTask(t *testing.T) *runnerv1.Task {
106+
resp, err := r.client.runnerServiceClient.FetchTask(context.Background(), connect.NewRequest(&runnerv1.FetchTaskRequest{
107+
TasksVersion: 0,
108+
}))
109+
assert.NoError(t, err)
110+
return resp.Msg.Task
111+
}
112+
113+
type taskExecPolicy struct {
114+
result runnerv1.Result
115+
outputs map[string]string
116+
logRows []*runnerv1.LogRow
117+
}
118+
119+
func (r *mockRunner) execTask(t *testing.T, task *runnerv1.Task, policy *taskExecPolicy) {
120+
for idx, lr := range policy.logRows {
121+
resp, err := r.client.runnerServiceClient.UpdateLog(context.Background(), connect.NewRequest(&runnerv1.UpdateLogRequest{
122+
TaskId: task.Id,
123+
Index: int64(idx),
124+
Rows: []*runnerv1.LogRow{lr},
125+
NoMore: idx == len(policy.logRows)-1,
126+
}))
127+
assert.NoError(t, err)
128+
assert.EqualValues(t, idx+1, resp.Msg.AckIndex)
129+
}
130+
sentOutputs := make(map[string]string, len(policy.outputs))
131+
for outputKey, outputValue := range policy.outputs {
132+
resp, err := r.client.runnerServiceClient.UpdateTask(context.Background(), connect.NewRequest(&runnerv1.UpdateTaskRequest{
133+
State: &runnerv1.TaskState{
134+
Id: task.Id,
135+
Result: runnerv1.Result_RESULT_UNSPECIFIED,
136+
},
137+
Outputs: map[string]string{outputKey: outputValue},
138+
}))
139+
assert.NoError(t, err)
140+
sentOutputs[outputKey] = outputValue
141+
assert.True(t, reflect.DeepEqual(sentOutputs, resp.Msg.SentOutputs))
142+
}
143+
resp, err := r.client.runnerServiceClient.UpdateTask(context.Background(), connect.NewRequest(&runnerv1.UpdateTaskRequest{
144+
State: &runnerv1.TaskState{
145+
Id: task.Id,
146+
Result: policy.result,
147+
StoppedAt: timestamppb.Now(),
148+
},
149+
}))
150+
assert.NoError(t, err)
151+
assert.Equal(t, policy.result, resp.Msg.State.Result)
152+
}

0 commit comments

Comments
 (0)