-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add mock runner and test jobs with needs
- Loading branch information
Showing
2 changed files
with
495 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,332 @@ | ||
// Copyright 2024 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package integration | ||
|
||
import ( | ||
"encoding/base64" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
"testing" | ||
"time" | ||
|
||
actions_model "code.gitea.io/gitea/models/actions" | ||
auth_model "code.gitea.io/gitea/models/auth" | ||
"code.gitea.io/gitea/models/unittest" | ||
user_model "code.gitea.io/gitea/models/user" | ||
api "code.gitea.io/gitea/modules/structs" | ||
|
||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestJobWithNeeds(t *testing.T) { | ||
testCases := []struct { | ||
treePath string | ||
fileContent string | ||
execPolicies map[string]*taskExecPolicy | ||
expectedStatuses map[string]string | ||
}{ | ||
{ | ||
treePath: ".gitea/workflows/job-with-needs.yml", | ||
fileContent: `name: job-with-needs | ||
on: | ||
push: | ||
paths: | ||
- '.gitea/workflows/job-with-needs.yml' | ||
jobs: | ||
job1: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- run: echo job1 | ||
job2: | ||
runs-on: ubuntu-latest | ||
needs: [job1] | ||
steps: | ||
- run: echo job2 | ||
`, | ||
execPolicies: map[string]*taskExecPolicy{ | ||
"job1": { | ||
result: runnerv1.Result_RESULT_SUCCESS, | ||
}, | ||
"job2": { | ||
result: runnerv1.Result_RESULT_SUCCESS, | ||
}, | ||
}, | ||
expectedStatuses: map[string]string{ | ||
"job1": actions_model.StatusSuccess.String(), | ||
"job2": actions_model.StatusSuccess.String(), | ||
}, | ||
}, | ||
{ | ||
treePath: ".gitea/workflows/job-with-needs-fail.yml", | ||
fileContent: `name: job-with-needs-fail | ||
on: | ||
push: | ||
paths: | ||
- '.gitea/workflows/job-with-needs-fail.yml' | ||
jobs: | ||
job1: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- run: echo job1 | ||
job2: | ||
runs-on: ubuntu-latest | ||
needs: [job1] | ||
steps: | ||
- run: echo job2 | ||
`, | ||
execPolicies: map[string]*taskExecPolicy{ | ||
"job1": { | ||
result: runnerv1.Result_RESULT_FAILURE, | ||
}, | ||
}, | ||
expectedStatuses: map[string]string{ | ||
"job1": actions_model.StatusFailure.String(), | ||
"job2": actions_model.StatusSkipped.String(), | ||
}, | ||
}, | ||
{ | ||
treePath: ".gitea/workflows/job-with-needs-fail-if.yml", | ||
fileContent: `name: job-with-needs-fail-if | ||
on: | ||
push: | ||
paths: | ||
- '.gitea/workflows/job-with-needs-fail-if.yml' | ||
jobs: | ||
job1: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- run: echo job1 | ||
job2: | ||
runs-on: ubuntu-latest | ||
needs: [job1] | ||
if: ${{ always() }} | ||
steps: | ||
- run: echo job2 | ||
`, | ||
execPolicies: map[string]*taskExecPolicy{ | ||
"job1": { | ||
result: runnerv1.Result_RESULT_FAILURE, | ||
}, | ||
"job2": { | ||
result: runnerv1.Result_RESULT_SUCCESS, | ||
}, | ||
}, | ||
expectedStatuses: map[string]string{ | ||
"job1": actions_model.StatusFailure.String(), | ||
"job2": actions_model.StatusSuccess.String(), | ||
}, | ||
}, | ||
} | ||
onGiteaRun(t, func(t *testing.T, u *url.URL) { | ||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | ||
session := loginUser(t, user2.Name) | ||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) | ||
|
||
apiRepo := createActionsTestRepo(t, token, "actions-jobs-with-needs", false) | ||
runner := newMockRunner() | ||
runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}) | ||
|
||
for _, tc := range testCases { | ||
t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) { | ||
// create the workflow file | ||
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent) | ||
fileResp := createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts) | ||
|
||
// fetch and execute task | ||
for i := 0; i < len(tc.execPolicies); i++ { | ||
task := runner.fetchTask(t) | ||
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id) | ||
policy := tc.execPolicies[jobName] | ||
assert.NotNil(t, policy) | ||
runner.execTask(t, task, policy) | ||
} | ||
|
||
// check result | ||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", user2.Name, apiRepo.Name)). | ||
AddTokenAuth(token) | ||
resp := MakeRequest(t, req, http.StatusOK) | ||
var actionTaskRespAfter api.ActionTaskResponse | ||
DecodeJSON(t, resp, &actionTaskRespAfter) | ||
for _, apiTask := range actionTaskRespAfter.Entries { | ||
if apiTask.HeadSHA != fileResp.Commit.SHA { | ||
continue | ||
} | ||
status := apiTask.Status | ||
assert.Equal(t, status, tc.expectedStatuses[apiTask.Name]) | ||
} | ||
}) | ||
} | ||
}) | ||
} | ||
|
||
func TestJobNeedsMatrix(t *testing.T) { | ||
testCases := []struct { | ||
treePath string | ||
fileContent string | ||
execPolicies map[string]*taskExecPolicy | ||
expectedOutputs map[string]map[string]string // jobID(string) => output(map[string]string) | ||
}{ | ||
{ | ||
treePath: ".gitea/workflows/jobs-outputs-with-matrix.yml", | ||
fileContent: `name: jobs-outputs-with-matrix | ||
on: | ||
push: | ||
paths: | ||
- '.gitea/workflows/jobs-outputs-with-matrix.yml' | ||
jobs: | ||
job1: | ||
runs-on: ubuntu-latest | ||
outputs: | ||
output_1: ${{ steps.gen_output.outputs.output_1 }} | ||
output_2: ${{ steps.gen_output.outputs.output_2 }} | ||
output_3: ${{ steps.gen_output.outputs.output_3 }} | ||
strategy: | ||
matrix: | ||
version: [1, 2, 3] | ||
steps: | ||
- name: Generate output | ||
id: gen_output | ||
run: | | ||
version="${{ matrix.version }}" | ||
echo "output_${version}=${version}" >> "$GITHUB_OUTPUT" | ||
job2: | ||
runs-on: ubuntu-latest | ||
needs: [job1] | ||
steps: | ||
- run: echo '${{ toJSON(needs.job1.outputs) }}' | ||
`, | ||
execPolicies: map[string]*taskExecPolicy{ | ||
"job1 (1)": { | ||
result: runnerv1.Result_RESULT_SUCCESS, | ||
outputs: map[string]string{ | ||
"output_1": "1", | ||
"output_2": "", | ||
"output_3": "", | ||
}, | ||
}, | ||
"job1 (2)": { | ||
result: runnerv1.Result_RESULT_SUCCESS, | ||
outputs: map[string]string{ | ||
"output_1": "", | ||
"output_2": "2", | ||
"output_3": "", | ||
}, | ||
}, | ||
"job1 (3)": { | ||
result: runnerv1.Result_RESULT_SUCCESS, | ||
outputs: map[string]string{ | ||
"output_1": "", | ||
"output_2": "", | ||
"output_3": "3", | ||
}, | ||
}, | ||
}, | ||
expectedOutputs: map[string]map[string]string{ | ||
"job1": { | ||
"output_1": "1", | ||
"output_2": "2", | ||
"output_3": "3", | ||
}, | ||
}, | ||
}, | ||
} | ||
onGiteaRun(t, func(t *testing.T, u *url.URL) { | ||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | ||
session := loginUser(t, user2.Name) | ||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) | ||
|
||
apiRepo := createActionsTestRepo(t, token, "actions-jobs-outputs-with-matrix", false) | ||
runner := newMockRunner() | ||
runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}) | ||
|
||
for _, tc := range testCases { | ||
t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) { | ||
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent) | ||
createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts) | ||
|
||
for i := 0; i < len(tc.execPolicies); i++ { | ||
task := runner.fetchTask(t) | ||
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id) | ||
policy := tc.execPolicies[jobName] | ||
assert.NotNil(t, policy) | ||
runner.execTask(t, task, policy) | ||
} | ||
|
||
job2Task := runner.fetchTask(t) | ||
needs := job2Task.Needs | ||
assert.Len(t, needs, len(tc.expectedOutputs)) | ||
for jobID, outputs := range tc.expectedOutputs { | ||
assert.Len(t, needs[jobID].Outputs, len(outputs)) | ||
for outputKey, outputValue := range outputs { | ||
assert.Equal(t, outputValue, needs[jobID].Outputs[outputKey]) | ||
} | ||
} | ||
}) | ||
} | ||
}) | ||
} | ||
|
||
func createActionsTestRepo(t *testing.T, authToken, repoName string, isPrivate bool) *api.Repository { | ||
req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{ | ||
Name: repoName, | ||
Private: isPrivate, | ||
Readme: "Default", | ||
AutoInit: true, | ||
DefaultBranch: "main", | ||
}).AddTokenAuth(authToken) | ||
resp := MakeRequest(t, req, http.StatusCreated) | ||
var apiRepo api.Repository | ||
DecodeJSON(t, resp, &apiRepo) | ||
return &apiRepo | ||
} | ||
|
||
func getWorkflowCreateFileOptions(u *user_model.User, branch, msg, content string) *api.CreateFileOptions { | ||
return &api.CreateFileOptions{ | ||
FileOptions: api.FileOptions{ | ||
BranchName: branch, | ||
Message: msg, | ||
Author: api.Identity{ | ||
Name: u.Name, | ||
Email: u.Email, | ||
}, | ||
Committer: api.Identity{ | ||
Name: u.Name, | ||
Email: u.Email, | ||
}, | ||
Dates: api.CommitDateOptions{ | ||
Author: time.Now(), | ||
Committer: time.Now(), | ||
}, | ||
}, | ||
ContentBase64: base64.StdEncoding.EncodeToString([]byte(content)), | ||
} | ||
} | ||
|
||
func createWorkflowFile(t *testing.T, authToken, ownerName, repoName, treePath string, opts *api.CreateFileOptions) *api.FileResponse { | ||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", ownerName, repoName, treePath), opts). | ||
AddTokenAuth(authToken) | ||
resp := MakeRequest(t, req, http.StatusCreated) | ||
var fileResponse api.FileResponse | ||
DecodeJSON(t, resp, &fileResponse) | ||
return &fileResponse | ||
} | ||
|
||
// getTaskJobNameByTaskID get the job name of the task by task ID | ||
// there is currently not an API for querying a task by ID so we have to list all the tasks | ||
func getTaskJobNameByTaskID(t *testing.T, authToken, ownerName, repoName string, taskID int64) string { | ||
// FIXME: we may need to query several pages | ||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", ownerName, repoName)). | ||
AddTokenAuth(authToken) | ||
resp := MakeRequest(t, req, http.StatusOK) | ||
var taskRespBefore api.ActionTaskResponse | ||
DecodeJSON(t, resp, &taskRespBefore) | ||
for _, apiTask := range taskRespBefore.Entries { | ||
if apiTask.ID == taskID { | ||
return apiTask.Name | ||
} | ||
} | ||
return "" | ||
} |
Oops, something went wrong.