mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
309 lines
9.6 KiB
Go
309 lines
9.6 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
actions_model "github.com/hanzoai/git/models/actions"
|
|
auth_model "github.com/hanzoai/git/models/auth"
|
|
repo_model "github.com/hanzoai/git/models/repo"
|
|
"github.com/hanzoai/git/models/unittest"
|
|
user_model "github.com/hanzoai/git/models/user"
|
|
runner_module "github.com/hanzoai/forge/modules/actions/runner"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/modules/storage"
|
|
"github.com/hanzoai/git/modules/test"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestDownloadTaskLogs(t *testing.T) {
|
|
now := time.Now()
|
|
testCases := []struct {
|
|
treePath string
|
|
fileContent string
|
|
outcome []*mockTaskOutcome
|
|
zstdEnabled bool
|
|
}{
|
|
{
|
|
treePath: ".hanzo/workflows/download-task-logs-zstd.yml",
|
|
fileContent: `name: download-task-logs-zstd
|
|
on:
|
|
push:
|
|
paths:
|
|
- '.hanzo/workflows/download-task-logs-zstd.yml'
|
|
jobs:
|
|
job1:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo job1 with zstd enabled
|
|
job2:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo job2 with zstd enabled
|
|
`,
|
|
outcome: []*mockTaskOutcome{
|
|
{
|
|
result: runner_module.Success,
|
|
logLines: []runner_module.Line{
|
|
{
|
|
Time: now.Add(1 * time.Second).UnixNano(),
|
|
Content: " \U0001F433 docker create image",
|
|
},
|
|
{
|
|
Time: now.Add(2 * time.Second).UnixNano(),
|
|
Content: "job1 zstd enabled",
|
|
},
|
|
{
|
|
Time: now.Add(3 * time.Second).UnixNano(),
|
|
Content: "\U0001F3C1 Job succeeded",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
result: runner_module.Success,
|
|
logLines: []runner_module.Line{
|
|
{
|
|
Time: now.Add(1 * time.Second).UnixNano(),
|
|
Content: " \U0001F433 docker create image",
|
|
},
|
|
{
|
|
Time: now.Add(2 * time.Second).UnixNano(),
|
|
Content: "job2 zstd enabled",
|
|
},
|
|
{
|
|
Time: now.Add(3 * time.Second).UnixNano(),
|
|
Content: "\U0001F3C1 Job succeeded",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
zstdEnabled: true,
|
|
},
|
|
{
|
|
treePath: ".hanzo/workflows/download-task-logs-no-zstd.yml",
|
|
fileContent: `name: download-task-logs-no-zstd
|
|
on:
|
|
push:
|
|
paths:
|
|
- '.hanzo/workflows/download-task-logs-no-zstd.yml'
|
|
jobs:
|
|
job1:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo job1 with zstd disabled
|
|
job2:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo job2 with zstd disabled
|
|
`,
|
|
outcome: []*mockTaskOutcome{
|
|
{
|
|
result: runner_module.Success,
|
|
logLines: []runner_module.Line{
|
|
{
|
|
Time: now.Add(4 * time.Second).UnixNano(),
|
|
Content: " \U0001F433 docker create image",
|
|
},
|
|
{
|
|
Time: now.Add(5 * time.Second).UnixNano(),
|
|
Content: "job1 zstd disabled",
|
|
},
|
|
{
|
|
Time: now.Add(6 * time.Second).UnixNano(),
|
|
Content: "\U0001F3C1 Job succeeded",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
result: runner_module.Success,
|
|
logLines: []runner_module.Line{
|
|
{
|
|
Time: now.Add(4 * time.Second).UnixNano(),
|
|
Content: " \U0001F433 docker create image",
|
|
},
|
|
{
|
|
Time: now.Add(5 * time.Second).UnixNano(),
|
|
Content: "job2 zstd disabled",
|
|
},
|
|
{
|
|
Time: now.Add(6 * time.Second).UnixNano(),
|
|
Content: "\U0001F3C1 Job succeeded",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
zstdEnabled: false,
|
|
},
|
|
}
|
|
onGitRun(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-download-task-logs", false)
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
|
|
runner := newMockRunner()
|
|
runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
|
|
|
for _, tc := range testCases {
|
|
t.Run("test "+tc.treePath, func(t *testing.T) {
|
|
var resetFunc func()
|
|
if tc.zstdEnabled {
|
|
resetFunc = test.MockVariableValue(&setting.Actions.LogCompression, "zstd")
|
|
assert.True(t, setting.Actions.LogCompression.IsZstd())
|
|
} else {
|
|
resetFunc = test.MockVariableValue(&setting.Actions.LogCompression, "none")
|
|
assert.False(t, setting.Actions.LogCompression.IsZstd())
|
|
}
|
|
|
|
// create the workflow file
|
|
opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+tc.treePath, tc.fileContent)
|
|
createWorkflowFile(t, token, user2.Name, repo.Name, tc.treePath, opts)
|
|
|
|
// fetch and execute tasks
|
|
for _, outcome := range tc.outcome {
|
|
task := runner.fetchTask(t)
|
|
runner.execTask(t, task, outcome)
|
|
|
|
// check whether the log file exists
|
|
logFileName := fmt.Sprintf("%s/%02x/%d.log", repo.FullName(), task.ID%256, task.ID)
|
|
if setting.Actions.LogCompression.IsZstd() {
|
|
logFileName += ".zst"
|
|
}
|
|
_, err := storage.Actions.Stat(logFileName)
|
|
assert.NoError(t, err)
|
|
|
|
_, job, run := getTaskAndJobAndRunByTaskID(t, task.ID)
|
|
|
|
// download task logs and check content
|
|
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, repo.Name, run.ID, job.ID)).
|
|
AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
logTextLines := strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
|
|
assert.Len(t, logTextLines, len(outcome.logLines))
|
|
for idx, lr := range outcome.logLines {
|
|
assert.Equal(
|
|
t,
|
|
fmt.Sprintf("%s %s", time.Unix(0, lr.Time).UTC().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content),
|
|
logTextLines[idx],
|
|
)
|
|
}
|
|
|
|
// download task logs from API and check content
|
|
req = NewRequest(t, "GET", fmt.Sprintf("/v1/repos/%s/%s/actions/jobs/%d/logs", user2.Name, repo.Name, job.ID)).
|
|
AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
logTextLines = strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
|
|
assert.Len(t, logTextLines, len(outcome.logLines))
|
|
for idx, lr := range outcome.logLines {
|
|
assert.Equal(
|
|
t,
|
|
fmt.Sprintf("%s %s", time.Unix(0, lr.Time).UTC().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content),
|
|
logTextLines[idx],
|
|
)
|
|
}
|
|
}
|
|
resetFunc()
|
|
})
|
|
}
|
|
|
|
t.Run("DownloadRerunTaskLogs", func(t *testing.T) {
|
|
treePath := ".hanzo/workflows/download-rerun-logs.yml"
|
|
fileContent := `name: download-rerun-logs
|
|
on:
|
|
push:
|
|
paths:
|
|
- '.hanzo/workflows/download-rerun-logs.yml'
|
|
jobs:
|
|
job1:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: echo job1
|
|
job2:
|
|
runs-on: ubuntu-latest
|
|
needs: [job1]
|
|
steps:
|
|
- run: echo job2
|
|
`
|
|
|
|
// create the workflow file
|
|
opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+treePath, fileContent)
|
|
createWorkflowFile(t, token, user2.Name, repo.Name, treePath, opts)
|
|
|
|
// first run
|
|
job1Task1 := runner.fetchTask(t)
|
|
_, job1, _ := getTaskAndJobAndRunByTaskID(t, job1Task1.ID)
|
|
runner.execTask(t, job1Task1, &mockTaskOutcome{
|
|
result: runner_module.Success,
|
|
logLines: []runner_module.Line{
|
|
{
|
|
Time: now.Add(1 * time.Second).UnixNano(),
|
|
Content: "job1 first run",
|
|
},
|
|
},
|
|
})
|
|
job2Task1 := runner.fetchTask(t)
|
|
_, job2, run := getTaskAndJobAndRunByTaskID(t, job2Task1.ID)
|
|
runner.execTask(t, job2Task1, &mockTaskOutcome{
|
|
result: runner_module.Success,
|
|
logLines: []runner_module.Line{
|
|
{
|
|
Time: now.Add(1 * time.Second).UnixNano(),
|
|
Content: "job2 first run",
|
|
},
|
|
},
|
|
})
|
|
|
|
// check job1 log
|
|
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, repo.Name, run.ID, job1.ID)).
|
|
AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
assert.Contains(t, resp.Body.String(), "job1 first run")
|
|
// check job2 log
|
|
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, repo.Name, run.ID, job2.ID)).
|
|
AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
assert.Contains(t, resp.Body.String(), "job2 first run")
|
|
|
|
// only rerun job2
|
|
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.ID, job2.ID))
|
|
session.MakeRequest(t, req, http.StatusOK)
|
|
job2TaskRerun := runner.fetchTask(t)
|
|
runner.execTask(t, job2TaskRerun, &mockTaskOutcome{
|
|
result: runner_module.Success,
|
|
logLines: []runner_module.Line{
|
|
{
|
|
Time: now.Add(1 * time.Second).UnixNano(),
|
|
Content: "job2 rerun",
|
|
},
|
|
},
|
|
})
|
|
|
|
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID})
|
|
job1Rerun := getLatestAttemptJobByTemplateJobID(t, run.ID, job1.ID)
|
|
assert.Equal(t, run.LatestAttemptID, job1Rerun.RunAttemptID)
|
|
job2Rerun := getLatestAttemptJobByTemplateJobID(t, run.ID, job2.ID)
|
|
assert.Equal(t, run.LatestAttemptID, job2Rerun.RunAttemptID)
|
|
|
|
// check job1 rerun log
|
|
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, repo.Name, run.ID, job1Rerun.ID)).
|
|
AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
assert.Contains(t, resp.Body.String(), "job1 first run") // should return the log of first run because job1 didn't rerun
|
|
// check job2 rerun log
|
|
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, repo.Name, run.ID, job2Rerun.ID)).
|
|
AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
assert.Contains(t, resp.Body.String(), "job2 rerun")
|
|
})
|
|
})
|
|
}
|