Skip to content

Commit 83d6eb0

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

File tree

2 files changed

+495
-0
lines changed

2 files changed

+495
-0
lines changed

tests/integration/actions_job_test.go

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
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+
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
125+
session := loginUser(t, user2.Name)
126+
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
127+
128+
apiRepo := createActionsTestRepo(t, token, "actions-jobs-with-needs", false)
129+
runner := newMockRunner()
130+
runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"})
131+
132+
for _, tc := range testCases {
133+
t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) {
134+
// create the workflow file
135+
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent)
136+
fileResp := createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts)
137+
138+
// fetch and execute task
139+
for i := 0; i < len(tc.execPolicies); i++ {
140+
task := runner.fetchTask(t)
141+
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
142+
policy := tc.execPolicies[jobName]
143+
assert.NotNil(t, policy)
144+
runner.execTask(t, task, policy)
145+
}
146+
147+
// check result
148+
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", user2.Name, apiRepo.Name)).
149+
AddTokenAuth(token)
150+
resp := MakeRequest(t, req, http.StatusOK)
151+
var actionTaskRespAfter api.ActionTaskResponse
152+
DecodeJSON(t, resp, &actionTaskRespAfter)
153+
for _, apiTask := range actionTaskRespAfter.Entries {
154+
if apiTask.HeadSHA != fileResp.Commit.SHA {
155+
continue
156+
}
157+
status := apiTask.Status
158+
assert.Equal(t, status, tc.expectedStatuses[apiTask.Name])
159+
}
160+
})
161+
}
162+
})
163+
}
164+
165+
func TestJobNeedsMatrix(t *testing.T) {
166+
testCases := []struct {
167+
treePath string
168+
fileContent string
169+
execPolicies map[string]*taskExecPolicy
170+
expectedOutputs map[string]map[string]string // jobID(string) => output(map[string]string)
171+
}{
172+
{
173+
treePath: ".gitea/workflows/jobs-outputs-with-matrix.yml",
174+
fileContent: `name: jobs-outputs-with-matrix
175+
on:
176+
push:
177+
paths:
178+
- '.gitea/workflows/jobs-outputs-with-matrix.yml'
179+
jobs:
180+
job1:
181+
runs-on: ubuntu-latest
182+
outputs:
183+
output_1: ${{ steps.gen_output.outputs.output_1 }}
184+
output_2: ${{ steps.gen_output.outputs.output_2 }}
185+
output_3: ${{ steps.gen_output.outputs.output_3 }}
186+
strategy:
187+
matrix:
188+
version: [1, 2, 3]
189+
steps:
190+
- name: Generate output
191+
id: gen_output
192+
run: |
193+
version="${{ matrix.version }}"
194+
echo "output_${version}=${version}" >> "$GITHUB_OUTPUT"
195+
job2:
196+
runs-on: ubuntu-latest
197+
needs: [job1]
198+
steps:
199+
- run: echo '${{ toJSON(needs.job1.outputs) }}'
200+
`,
201+
execPolicies: map[string]*taskExecPolicy{
202+
"job1 (1)": {
203+
result: runnerv1.Result_RESULT_SUCCESS,
204+
outputs: map[string]string{
205+
"output_1": "1",
206+
"output_2": "",
207+
"output_3": "",
208+
},
209+
},
210+
"job1 (2)": {
211+
result: runnerv1.Result_RESULT_SUCCESS,
212+
outputs: map[string]string{
213+
"output_1": "",
214+
"output_2": "2",
215+
"output_3": "",
216+
},
217+
},
218+
"job1 (3)": {
219+
result: runnerv1.Result_RESULT_SUCCESS,
220+
outputs: map[string]string{
221+
"output_1": "",
222+
"output_2": "",
223+
"output_3": "3",
224+
},
225+
},
226+
},
227+
expectedOutputs: map[string]map[string]string{
228+
"job1": {
229+
"output_1": "1",
230+
"output_2": "2",
231+
"output_3": "3",
232+
},
233+
},
234+
},
235+
}
236+
onGiteaRun(t, func(t *testing.T, u *url.URL) {
237+
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
238+
session := loginUser(t, user2.Name)
239+
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
240+
241+
apiRepo := createActionsTestRepo(t, token, "actions-jobs-outputs-with-matrix", false)
242+
runner := newMockRunner()
243+
runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"})
244+
245+
for _, tc := range testCases {
246+
t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) {
247+
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent)
248+
createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts)
249+
250+
for i := 0; i < len(tc.execPolicies); i++ {
251+
task := runner.fetchTask(t)
252+
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
253+
policy := tc.execPolicies[jobName]
254+
assert.NotNil(t, policy)
255+
runner.execTask(t, task, policy)
256+
}
257+
258+
job2Task := runner.fetchTask(t)
259+
needs := job2Task.Needs
260+
assert.Len(t, needs, len(tc.expectedOutputs))
261+
for jobID, outputs := range tc.expectedOutputs {
262+
assert.Len(t, needs[jobID].Outputs, len(outputs))
263+
for outputKey, outputValue := range outputs {
264+
assert.Equal(t, outputValue, needs[jobID].Outputs[outputKey])
265+
}
266+
}
267+
})
268+
}
269+
})
270+
}
271+
272+
func createActionsTestRepo(t *testing.T, authToken, repoName string, isPrivate bool) *api.Repository {
273+
req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
274+
Name: repoName,
275+
Private: isPrivate,
276+
Readme: "Default",
277+
AutoInit: true,
278+
DefaultBranch: "main",
279+
}).AddTokenAuth(authToken)
280+
resp := MakeRequest(t, req, http.StatusCreated)
281+
var apiRepo api.Repository
282+
DecodeJSON(t, resp, &apiRepo)
283+
return &apiRepo
284+
}
285+
286+
func getWorkflowCreateFileOptions(u *user_model.User, branch, msg, content string) *api.CreateFileOptions {
287+
return &api.CreateFileOptions{
288+
FileOptions: api.FileOptions{
289+
BranchName: branch,
290+
Message: msg,
291+
Author: api.Identity{
292+
Name: u.Name,
293+
Email: u.Email,
294+
},
295+
Committer: api.Identity{
296+
Name: u.Name,
297+
Email: u.Email,
298+
},
299+
Dates: api.CommitDateOptions{
300+
Author: time.Now(),
301+
Committer: time.Now(),
302+
},
303+
},
304+
ContentBase64: base64.StdEncoding.EncodeToString([]byte(content)),
305+
}
306+
}
307+
308+
func createWorkflowFile(t *testing.T, authToken, ownerName, repoName, treePath string, opts *api.CreateFileOptions) *api.FileResponse {
309+
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", ownerName, repoName, treePath), opts).
310+
AddTokenAuth(authToken)
311+
resp := MakeRequest(t, req, http.StatusCreated)
312+
var fileResponse api.FileResponse
313+
DecodeJSON(t, resp, &fileResponse)
314+
return &fileResponse
315+
}
316+
317+
// getTaskJobNameByTaskID get the job name of the task by task ID
318+
// there is currently not an API for querying a task by ID so we have to list all the tasks
319+
func getTaskJobNameByTaskID(t *testing.T, authToken, ownerName, repoName string, taskID int64) string {
320+
// FIXME: we may need to query several pages
321+
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", ownerName, repoName)).
322+
AddTokenAuth(authToken)
323+
resp := MakeRequest(t, req, http.StatusOK)
324+
var taskRespBefore api.ActionTaskResponse
325+
DecodeJSON(t, resp, &taskRespBefore)
326+
for _, apiTask := range taskRespBefore.Entries {
327+
if apiTask.ID == taskID {
328+
return apiTask.Name
329+
}
330+
}
331+
return ""
332+
}

0 commit comments

Comments
 (0)