Skip to content

Commit 6950126

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

File tree

2 files changed

+365
-0
lines changed

2 files changed

+365
-0
lines changed

tests/integration/actions_job_test.go

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

0 commit comments

Comments
 (0)