初始提交: Gitea 项目代码
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/json"
|
||||
api "gitea.dev/modules/structs"
|
||||
|
||||
act_model "gitea.com/gitea/runner/act/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEvaluateRunConcurrency_RunIDFallback(t *testing.T) {
|
||||
// Unit-level check that EvaluateRunConcurrencyFillModel resolves github.run_id from run.ID.
|
||||
// The full-flow regression (run.ID non-zero by evaluation time) is TestPrepareRunAndInsert_ExpressionsSeeRunID.
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
ctx := t.Context()
|
||||
|
||||
runA := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 791})
|
||||
runB := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 792})
|
||||
|
||||
attemptA := &actions_model.ActionRunAttempt{RepoID: runA.RepoID, RunID: runA.ID, Attempt: 1}
|
||||
attemptB := &actions_model.ActionRunAttempt{RepoID: runB.RepoID, RunID: runB.ID, Attempt: 1}
|
||||
|
||||
expr := &act_model.RawConcurrency{
|
||||
Group: "${{ github.workflow }}-${{ github.head_ref || github.run_id }}",
|
||||
CancelInProgress: "true",
|
||||
}
|
||||
|
||||
assert.NoError(t, EvaluateRunConcurrencyFillModel(ctx, runA, attemptA, expr, nil, nil))
|
||||
assert.NoError(t, EvaluateRunConcurrencyFillModel(ctx, runB, attemptB, expr, nil, nil))
|
||||
|
||||
assert.Contains(t, attemptA.ConcurrencyGroup, "791")
|
||||
assert.Contains(t, attemptB.ConcurrencyGroup, "792")
|
||||
assert.NotEqual(t, attemptA.ConcurrencyGroup, attemptB.ConcurrencyGroup)
|
||||
}
|
||||
|
||||
func TestPrepareRunAndInsert_ExpressionsSeeRunID(t *testing.T) {
|
||||
// Regression for the cross-branch concurrency leak: github.run_id must be available during both
|
||||
// jobparser.Parse (run-name) and concurrency evaluation; inserting run after either leaves run.ID at 0.
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
ctx := t.Context()
|
||||
|
||||
content := []byte(`name: cross-branch
|
||||
run-name: "Run ${{ github.run_id }}"
|
||||
on: push
|
||||
concurrency:
|
||||
group: group-${{ github.run_id }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
hello:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hi
|
||||
`)
|
||||
|
||||
run := &actions_model.ActionRun{
|
||||
Title: "before parse",
|
||||
RepoID: 4,
|
||||
OwnerID: 1,
|
||||
WorkflowID: "expr-runid.yaml",
|
||||
TriggerUserID: 1,
|
||||
Ref: "refs/heads/master",
|
||||
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
|
||||
Event: "push",
|
||||
TriggerEvent: "push",
|
||||
EventPayload: "{}",
|
||||
}
|
||||
require.NoError(t, PrepareRunAndInsert(ctx, content, run, nil))
|
||||
require.Positive(t, run.ID)
|
||||
|
||||
persisted := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID})
|
||||
runIDStr := strconv.FormatInt(run.ID, 10)
|
||||
assert.Equal(t, "Run "+runIDStr, persisted.Title)
|
||||
// ConcurrencyGroup lives on the latest attempt after migration v331.
|
||||
require.Positive(t, persisted.LatestAttemptID)
|
||||
attempt := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunAttempt{ID: persisted.LatestAttemptID})
|
||||
assert.Equal(t, "group-"+runIDStr, attempt.ConcurrencyGroup)
|
||||
// Rerun reads raw_concurrency from the DB to re-evaluate the group;
|
||||
// see services/actions/rerun.go. Must survive the insert.
|
||||
assert.NotEmpty(t, persisted.RawConcurrency)
|
||||
}
|
||||
|
||||
func TestComputeReusableCallerOutputs(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
ctx := t.Context()
|
||||
|
||||
var nextRunIndex int64 = 9001
|
||||
insertRun := func(t *testing.T, workflowID string) *actions_model.ActionRun {
|
||||
t.Helper()
|
||||
run := &actions_model.ActionRun{
|
||||
Title: "reusable-out",
|
||||
RepoID: 4,
|
||||
Index: nextRunIndex,
|
||||
OwnerID: 1,
|
||||
WorkflowID: workflowID,
|
||||
TriggerUserID: 1,
|
||||
Ref: "refs/heads/master",
|
||||
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
|
||||
Event: "push",
|
||||
TriggerEvent: "push",
|
||||
EventPayload: "{}",
|
||||
Status: actions_model.StatusSuccess,
|
||||
}
|
||||
nextRunIndex++
|
||||
require.NoError(t, db.Insert(ctx, run))
|
||||
return run
|
||||
}
|
||||
|
||||
insertCaller := func(t *testing.T, run *actions_model.ActionRun, jobID string, parentID int64, content, callPayload string) *actions_model.ActionRunJob {
|
||||
t.Helper()
|
||||
job := &actions_model.ActionRunJob{
|
||||
RunID: run.ID,
|
||||
RepoID: run.RepoID,
|
||||
OwnerID: run.OwnerID,
|
||||
CommitSHA: run.CommitSHA,
|
||||
Name: jobID,
|
||||
JobID: jobID,
|
||||
Attempt: 1,
|
||||
Status: actions_model.StatusSuccess,
|
||||
ParentJobID: parentID,
|
||||
IsReusableCaller: true,
|
||||
IsExpanded: true,
|
||||
ReusableWorkflowContent: []byte(content),
|
||||
CallPayload: callPayload,
|
||||
}
|
||||
require.NoError(t, db.Insert(ctx, job))
|
||||
return job
|
||||
}
|
||||
|
||||
// Each call to insertChildJobAndTask with non-empty outputs allocates a fresh TaskID
|
||||
// so its action_task_output rows stay isolated per subtest.
|
||||
var nextTaskID int64 = 90001
|
||||
insertChildJobAndTask := func(t *testing.T, run *actions_model.ActionRun, jobID string, parentID int64, outputs map[string]string) *actions_model.ActionRunJob {
|
||||
t.Helper()
|
||||
var taskID int64
|
||||
if len(outputs) > 0 {
|
||||
taskID = nextTaskID
|
||||
nextTaskID++
|
||||
}
|
||||
job := &actions_model.ActionRunJob{
|
||||
RunID: run.ID,
|
||||
RepoID: run.RepoID,
|
||||
OwnerID: run.OwnerID,
|
||||
CommitSHA: run.CommitSHA,
|
||||
Name: jobID,
|
||||
JobID: jobID,
|
||||
Attempt: 1,
|
||||
Status: actions_model.StatusSuccess,
|
||||
ParentJobID: parentID,
|
||||
TaskID: taskID,
|
||||
}
|
||||
require.NoError(t, db.Insert(ctx, job))
|
||||
for k, v := range outputs {
|
||||
require.NoError(t, db.Insert(ctx, &actions_model.ActionTaskOutput{
|
||||
TaskID: taskID,
|
||||
OutputKey: k,
|
||||
OutputValue: v,
|
||||
}))
|
||||
}
|
||||
return job
|
||||
}
|
||||
|
||||
// childrenByParentOfRun returns the run's jobs indexed by ParentJobID, the shape computeReusableCallerOutputs expects.
|
||||
childrenByParentOfRun := func(t *testing.T, runID int64) map[int64][]*actions_model.ActionRunJob {
|
||||
t.Helper()
|
||||
all, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: runID})
|
||||
require.NoError(t, err)
|
||||
index := make(map[int64][]*actions_model.ActionRunJob)
|
||||
for _, j := range all {
|
||||
if j.ParentJobID != 0 {
|
||||
index[j.ParentJobID] = append(index[j.ParentJobID], j)
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
t.Run("returns empty when callee declares no outputs", func(t *testing.T) {
|
||||
run := insertRun(t, "no-outputs.yaml")
|
||||
caller := insertCaller(t, run, "caller", 0, `on:
|
||||
workflow_call:
|
||||
outputs: {}
|
||||
`, "")
|
||||
out, err := computeReusableCallerOutputs(ctx, caller, childrenByParentOfRun(t, run.ID))
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, out)
|
||||
})
|
||||
|
||||
t.Run("unexpanded (skipped) caller yields empty outputs without error", func(t *testing.T) {
|
||||
run := insertRun(t, "skipped-caller.yaml")
|
||||
// A reusable caller skipped before expansion: IsExpanded=false, empty ReusableWorkflowContent, no children.
|
||||
caller := &actions_model.ActionRunJob{
|
||||
RunID: run.ID,
|
||||
RepoID: run.RepoID,
|
||||
OwnerID: run.OwnerID,
|
||||
CommitSHA: run.CommitSHA,
|
||||
Name: "caller",
|
||||
JobID: "caller",
|
||||
Attempt: 1,
|
||||
Status: actions_model.StatusSkipped,
|
||||
IsReusableCaller: true,
|
||||
IsExpanded: false,
|
||||
}
|
||||
require.NoError(t, db.Insert(ctx, caller))
|
||||
out, err := computeReusableCallerOutputs(ctx, caller, childrenByParentOfRun(t, run.ID))
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, out)
|
||||
})
|
||||
|
||||
t.Run("literal output value passes through", func(t *testing.T) {
|
||||
run := insertRun(t, "literal-out.yaml")
|
||||
caller := insertCaller(t, run, "caller", 0, `on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
hello:
|
||||
value: world
|
||||
`, "")
|
||||
out, err := computeReusableCallerOutputs(ctx, caller, childrenByParentOfRun(t, run.ID))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]string{"hello": "world"}, out)
|
||||
})
|
||||
|
||||
t.Run("output expression reads child task outputs", func(t *testing.T) {
|
||||
run := insertRun(t, "child-out.yaml")
|
||||
caller := insertCaller(t, run, "caller", 0, `on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
result:
|
||||
value: ${{ jobs.child.outputs.foo }}
|
||||
`, "")
|
||||
insertChildJobAndTask(t, run, "child", caller.ID, map[string]string{"foo": "bar"})
|
||||
|
||||
out, err := computeReusableCallerOutputs(ctx, caller, childrenByParentOfRun(t, run.ID))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]string{"result": "bar"}, out)
|
||||
})
|
||||
|
||||
t.Run("CallPayload inputs reachable in output expression", func(t *testing.T) {
|
||||
run := insertRun(t, "payload-out.yaml")
|
||||
payload, err := json.Marshal(api.WorkflowCallPayload{
|
||||
Inputs: map[string]any{"env": "staging"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
caller := insertCaller(t, run, "caller", 0, `on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
env:
|
||||
type: string
|
||||
outputs:
|
||||
env:
|
||||
value: ${{ inputs.env }}
|
||||
`, string(payload))
|
||||
|
||||
out, err := computeReusableCallerOutputs(ctx, caller, childrenByParentOfRun(t, run.ID))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]string{"env": "staging"}, out)
|
||||
})
|
||||
|
||||
t.Run("nested caller outputs propagate to outer", func(t *testing.T) {
|
||||
run := insertRun(t, "nested-out.yaml")
|
||||
outer := insertCaller(t, run, "outer", 0, `on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
bubbled:
|
||||
value: ${{ jobs.inner.outputs.up }}
|
||||
`, "")
|
||||
inner := insertCaller(t, run, "inner", outer.ID, `on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
up:
|
||||
value: ${{ jobs.leaf.outputs.foo }}
|
||||
`, "")
|
||||
insertChildJobAndTask(t, run, "leaf", inner.ID, map[string]string{"foo": "bubble-value"})
|
||||
|
||||
out, err := computeReusableCallerOutputs(ctx, outer, childrenByParentOfRun(t, run.ID))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]string{"bubbled": "bubble-value"}, out)
|
||||
})
|
||||
|
||||
t.Run("matrix children with same JobID prefer non-empty values", func(t *testing.T) {
|
||||
run := insertRun(t, "matrix-out.yaml")
|
||||
caller := insertCaller(t, run, "caller", 0, `on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
foo:
|
||||
value: ${{ jobs.matrix.outputs.foo }}
|
||||
`, "")
|
||||
insertChildJobAndTask(t, run, "matrix", caller.ID, map[string]string{"foo": ""})
|
||||
insertChildJobAndTask(t, run, "matrix", caller.ID, map[string]string{"foo": "filled"})
|
||||
|
||||
out, err := computeReusableCallerOutputs(ctx, caller, childrenByParentOfRun(t, run.ID))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]string{"foo": "filled"}, out)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFindTaskNeeds(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 51})
|
||||
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: task.JobID})
|
||||
|
||||
ret, err := FindTaskNeeds(t.Context(), job)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, ret, 1)
|
||||
assert.Contains(t, ret, "job1")
|
||||
assert.Len(t, ret["job1"].Outputs, 2)
|
||||
assert.Equal(t, "abc", ret["job1"].Outputs["output_a"])
|
||||
assert.Equal(t, "bbb", ret["job1"].Outputs["output_b"])
|
||||
}
|
||||
Reference in New Issue
Block a user