mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-31 22:58:18 +00:00
11d0ed699b
Follow-up to https://github.com/go-gitea/gitea/pull/36564 (dynamic matrix) and https://github.com/go-gitea/gitea/pull/36357 (max-parallel), fixing issues found reviewing the two features together. - **A placeholder could stall its run forever.** Its payload keeps the raw matrix but loses its `needs`, so `ParseJob` re-expanded it instead of reading it back — fatal for `include: ${{ fromJson(needs.*.outputs.*) }}`. - **An `if:` reading `matrix.*` skipped the whole job**, with or without the `${{ }}`. It now reduces to the needs gate, except under `always()`/`failure()`/`cancelled()`, and each combination is decided on its own values once the matrix expands. - **Dependents could be skipped before the combinations ran**, since inserted siblings are absent from the resolver's job set. The pass now stops after an insert and defers to the re-emit it schedules. - **Expansion failures stranded the placeholder.** A retryable one is returned so the queue retries it; a malformed payload fails the job instead of requeueing forever. - **Rerun could rewind a pass-through row** into a raw placeholder keeping its old terminal status, which nothing expands. Now gated on the anchor itself. Plus: `max-parallel` distinguishes an unevaluated `${{ }}` (debug) from a non-numeric literal (warn — it silently drops the cap). Co-authored-by: Zettat123 <zettat123@gmail.com> Co-authored-by: silverwind <me@silverwind.io>
456 lines
20 KiB
Go
456 lines
20 KiB
Go
// Copyright 2024 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package actions
|
|
|
|
import (
|
|
"testing"
|
|
|
|
actions_model "gitea.dev/models/actions"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/container"
|
|
"gitea.dev/modules/util"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestGetFailedJobsForRerun(t *testing.T) {
|
|
makeJob := func(id int64, jobID string, status actions_model.Status, needs ...string) *actions_model.ActionRunJob {
|
|
return &actions_model.ActionRunJob{ID: id, JobID: jobID, Status: status, Needs: needs}
|
|
}
|
|
|
|
t.Run("no failed jobs returns empty", func(t *testing.T) {
|
|
jobs := []*actions_model.ActionRunJob{
|
|
makeJob(1, "job1", actions_model.StatusSuccess),
|
|
makeJob(2, "job2", actions_model.StatusSkipped, "job1"),
|
|
}
|
|
assert.Empty(t, GetFailedJobsForRerun(jobs))
|
|
})
|
|
|
|
t.Run("single failed job with no dependents", func(t *testing.T) {
|
|
job1 := makeJob(1, "job1", actions_model.StatusFailure)
|
|
job2 := makeJob(2, "job2", actions_model.StatusSuccess)
|
|
jobs := []*actions_model.ActionRunJob{job1, job2}
|
|
|
|
result := GetFailedJobsForRerun(jobs)
|
|
assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1}, result)
|
|
})
|
|
|
|
t.Run("failed job does not pull in downstream dependents", func(t *testing.T) {
|
|
job1 := makeJob(1, "job1", actions_model.StatusFailure)
|
|
job2 := makeJob(2, "job2", actions_model.StatusSkipped, "job1")
|
|
job3 := makeJob(3, "job3", actions_model.StatusSkipped, "job2")
|
|
job4 := makeJob(4, "job4", actions_model.StatusSuccess) // unrelated, must not appear
|
|
jobs := []*actions_model.ActionRunJob{job1, job2, job3, job4}
|
|
|
|
result := GetFailedJobsForRerun(jobs)
|
|
assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1}, result)
|
|
})
|
|
|
|
t.Run("multiple failed jobs are returned directly", func(t *testing.T) {
|
|
job1 := makeJob(1, "job1", actions_model.StatusFailure)
|
|
job2 := makeJob(2, "job2", actions_model.StatusFailure)
|
|
job3 := makeJob(3, "job3", actions_model.StatusSkipped, "job1")
|
|
job4 := makeJob(4, "job4", actions_model.StatusSkipped, "job2")
|
|
jobs := []*actions_model.ActionRunJob{job1, job2, job3, job4}
|
|
|
|
result := GetFailedJobsForRerun(jobs)
|
|
assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1, job2}, result)
|
|
})
|
|
|
|
t.Run("shared downstream dependent is not included", func(t *testing.T) {
|
|
job1 := makeJob(1, "job1", actions_model.StatusFailure)
|
|
job2 := makeJob(2, "job2", actions_model.StatusFailure)
|
|
job3 := makeJob(3, "job3", actions_model.StatusSkipped, "job1", "job2")
|
|
jobs := []*actions_model.ActionRunJob{job1, job2, job3}
|
|
|
|
result := GetFailedJobsForRerun(jobs)
|
|
assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1, job2}, result)
|
|
assert.Len(t, result, 2)
|
|
})
|
|
|
|
t.Run("successful downstream job of a failed job is not included", func(t *testing.T) {
|
|
job1 := makeJob(1, "job1", actions_model.StatusFailure)
|
|
job2 := makeJob(2, "job2", actions_model.StatusSuccess, "job1")
|
|
jobs := []*actions_model.ActionRunJob{job1, job2}
|
|
|
|
result := GetFailedJobsForRerun(jobs)
|
|
assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1}, result)
|
|
})
|
|
}
|
|
|
|
func TestCloneRunJobForAttempt(t *testing.T) {
|
|
attempt := &actions_model.ActionRunAttempt{ID: 42, Attempt: 2}
|
|
|
|
t.Run("preserves continue-on-error", func(t *testing.T) {
|
|
template := &actions_model.ActionRunJob{ContinueOnError: true, Status: actions_model.StatusFailure}
|
|
clone := cloneRunJobForAttempt(template, attempt)
|
|
assert.True(t, clone.ContinueOnError)
|
|
})
|
|
|
|
t.Run("defaults to false when template has it unset", func(t *testing.T) {
|
|
template := &actions_model.ActionRunJob{ContinueOnError: false}
|
|
clone := cloneRunJobForAttempt(template, attempt)
|
|
assert.False(t, clone.ContinueOnError)
|
|
})
|
|
|
|
// without this the rerun of a capped matrix would run unlimited
|
|
t.Run("preserves max-parallel", func(t *testing.T) {
|
|
template := &actions_model.ActionRunJob{MaxParallel: 3}
|
|
clone := cloneRunJobForAttempt(template, attempt)
|
|
assert.Equal(t, 3, clone.MaxParallel)
|
|
})
|
|
}
|
|
|
|
func TestRerunValidation(t *testing.T) {
|
|
runningRun := &actions_model.ActionRun{Status: actions_model.StatusRunning}
|
|
|
|
t.Run("RerunWorkflowRunJobs rejects a non-done run", func(t *testing.T) {
|
|
jobs := []*actions_model.ActionRunJob{
|
|
{ID: 1, JobID: "job1"},
|
|
}
|
|
_, err := RerunWorkflowRunJobs(t.Context(), nil, runningRun, &user_model.User{ID: 1}, jobs)
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, util.ErrInvalidArgument)
|
|
})
|
|
|
|
t.Run("RerunWorkflowRunJobs rejects a non-done run when failed jobs exist", func(t *testing.T) {
|
|
jobs := []*actions_model.ActionRunJob{
|
|
{ID: 1, JobID: "job1", Status: actions_model.StatusFailure},
|
|
}
|
|
_, err := RerunWorkflowRunJobs(t.Context(), nil, runningRun, &user_model.User{ID: 1}, GetFailedJobsForRerun(jobs))
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, util.ErrInvalidArgument)
|
|
})
|
|
}
|
|
|
|
func TestRerunPlan(t *testing.T) {
|
|
// "verify" appears in two scopes (inner caller under deploy, and top-level) so scope-blind matching would fail here.
|
|
|
|
// build id=101, attemptJobID=1
|
|
// test id=102, attemptJobID=2, needs=[build]
|
|
// deploy id=103, attemptJobID=3, caller
|
|
// ├── validate id=104, attemptJobID=4, parent=103
|
|
// ├── push id=105, attemptJobID=5, parent=103, needs=[validate]
|
|
// ├── verify id=106, attemptJobID=6, parent=103, caller, needs=[push]
|
|
// │ ├── smoke-test id=107, attemptJobID=7, parent=106
|
|
// │ └── cleanup id=108, attemptJobID=8, parent=106, needs=[smoke-test]
|
|
// └── finish-deploy id=109, attemptJobID=9, parent=103, needs=[verify]
|
|
// verify id=110, attemptJobID=10, needs=[deploy] (top-level, same JobID)
|
|
|
|
buildJob := templateJob(101, 1, "build", 0, false)
|
|
testJob := templateJob(102, 2, "test", 0, false, "build")
|
|
deployJob := templateJob(103, 3, "deploy", 0, true)
|
|
validateJob := templateJob(104, 4, "validate", 103, false)
|
|
pushJob := templateJob(105, 5, "push", 103, false, "validate")
|
|
verifyInnerJob := templateJob(106, 6, "verify", 103, true, "push")
|
|
smokeTestJob := templateJob(107, 7, "smoke-test", 106, false)
|
|
cleanupJob := templateJob(108, 8, "cleanup", 106, false, "smoke-test")
|
|
finishDeployJob := templateJob(109, 9, "finish-deploy", 103, false, "verify")
|
|
verifyTopJob := templateJob(110, 10, "verify", 0, false, "deploy")
|
|
|
|
jobs := []*actions_model.ActionRunJob{
|
|
buildJob, testJob, deployJob, validateJob, pushJob,
|
|
verifyInnerJob, smokeTestJob, cleanupJob,
|
|
finishDeployJob, verifyTopJob,
|
|
}
|
|
|
|
t.Run("ExpandRerunJobIDs", func(t *testing.T) {
|
|
t.Run("empty jobsToRerun reruns every template job, no ancestors", func(t *testing.T) {
|
|
plan := &rerunPlan{templateJobs: jobs}
|
|
require.NoError(t, plan.expandRerunJobIDs(nil))
|
|
|
|
assert.ElementsMatch(t, attemptJobIDsOf(jobs...), plan.rerunAttemptJobIDs.Values())
|
|
assert.Empty(t, plan.ancestorAttemptJobIDs)
|
|
})
|
|
|
|
t.Run("same-scope downstream BFS pulls in dependents", func(t *testing.T) {
|
|
// a -> b -> c (chain), d unrelated.
|
|
a := templateJob(101, 1, "a", 0, false)
|
|
b := templateJob(102, 2, "b", 0, false, "a")
|
|
c := templateJob(103, 3, "c", 0, false, "b")
|
|
d := templateJob(104, 4, "d", 0, false)
|
|
plan := &rerunPlan{templateJobs: []*actions_model.ActionRunJob{a, b, c, d}}
|
|
require.NoError(t, plan.expandRerunJobIDs([]*actions_model.ActionRunJob{a}))
|
|
|
|
assert.ElementsMatch(t, attemptJobIDsOf(a, b, c), plan.rerunAttemptJobIDs.Values())
|
|
assert.Empty(t, plan.ancestorAttemptJobIDs)
|
|
})
|
|
|
|
t.Run("rerun a deep child escalates across reusable scopes", func(t *testing.T) {
|
|
plan := &rerunPlan{templateJobs: jobs}
|
|
require.NoError(t, plan.expandRerunJobIDs([]*actions_model.ActionRunJob{smokeTestJob}))
|
|
|
|
// rerun: smoke-test (selected), cleanup (same-scope downstream),
|
|
// finish-deploy (deploy-scope sibling of inner verify ancestor),
|
|
// top-level verify (top-scope sibling of deploy ancestor).
|
|
assert.ElementsMatch(t,
|
|
attemptJobIDsOf(smokeTestJob, cleanupJob, finishDeployJob, verifyTopJob),
|
|
plan.rerunAttemptJobIDs.Values())
|
|
|
|
// ancestors: inner verify and deploy
|
|
assert.ElementsMatch(t, attemptJobIDsOf(verifyInnerJob, deployJob), plan.ancestorAttemptJobIDs.Values())
|
|
})
|
|
|
|
t.Run("rerun a top-level caller resets only itself and same-scope dependents", func(t *testing.T) {
|
|
plan := &rerunPlan{templateJobs: jobs}
|
|
require.NoError(t, plan.expandRerunJobIDs([]*actions_model.ActionRunJob{deployJob}))
|
|
|
|
// rerun: deploy (selected) + top-level verify (needs:[deploy]).
|
|
assert.ElementsMatch(t, attemptJobIDsOf(deployJob, verifyTopJob), plan.rerunAttemptJobIDs.Values())
|
|
// deploy is top-level so no ancestors.
|
|
assert.Empty(t, plan.ancestorAttemptJobIDs)
|
|
})
|
|
|
|
t.Run("rerun a nested caller escalates one level", func(t *testing.T) {
|
|
plan := &rerunPlan{templateJobs: jobs}
|
|
require.NoError(t, plan.expandRerunJobIDs([]*actions_model.ActionRunJob{verifyInnerJob}))
|
|
|
|
// inner verify (selected) -> finish-deploy (deploy-scope dep) -> top-level verify (top-scope dep of deploy).
|
|
assert.ElementsMatch(t,
|
|
attemptJobIDsOf(verifyInnerJob, finishDeployJob, verifyTopJob),
|
|
plan.rerunAttemptJobIDs.Values())
|
|
// deploy is the only ancestor (one level up from inner verify).
|
|
assert.ElementsMatch(t, attemptJobIDsOf(deployJob), plan.ancestorAttemptJobIDs.Values())
|
|
})
|
|
|
|
t.Run("selecting one same-name job leaves the other-scope same-name job alone", func(t *testing.T) {
|
|
// Selecting the top-level "verify" must not pull in the same-named inner one or its descendants.
|
|
plan := &rerunPlan{templateJobs: jobs}
|
|
require.NoError(t, plan.expandRerunJobIDs([]*actions_model.ActionRunJob{verifyTopJob}))
|
|
|
|
// Only the top-level verify is rerun.
|
|
assert.ElementsMatch(t, attemptJobIDsOf(verifyTopJob), plan.rerunAttemptJobIDs.Values())
|
|
assert.Empty(t, plan.ancestorAttemptJobIDs)
|
|
})
|
|
|
|
t.Run("a caller is rerun when a sibling it needs is selected", func(t *testing.T) {
|
|
plan := &rerunPlan{templateJobs: jobs}
|
|
require.NoError(t, plan.expandRerunJobIDs([]*actions_model.ActionRunJob{pushJob}))
|
|
|
|
assert.ElementsMatch(t,
|
|
attemptJobIDsOf(pushJob, verifyInnerJob, finishDeployJob, verifyTopJob),
|
|
plan.rerunAttemptJobIDs.Values())
|
|
assert.ElementsMatch(t, attemptJobIDsOf(deployJob), plan.ancestorAttemptJobIDs.Values())
|
|
|
|
// Confirm the downstream effect: verify(inner) is a reset caller, so its children's DB row IDs are marked for skip-clone.
|
|
assert.ElementsMatch(t, rowIDsOf(smokeTestJob, cleanupJob), plan.collectResetCallerDescendants().Values())
|
|
})
|
|
|
|
t.Run("multiple selections converge", func(t *testing.T) {
|
|
plan := &rerunPlan{templateJobs: jobs}
|
|
require.NoError(t, plan.expandRerunJobIDs([]*actions_model.ActionRunJob{deployJob, smokeTestJob}))
|
|
|
|
assert.ElementsMatch(t, attemptJobIDsOf(deployJob, smokeTestJob, cleanupJob, finishDeployJob, verifyTopJob), plan.rerunAttemptJobIDs.Values())
|
|
assert.Empty(t, plan.ancestorAttemptJobIDs)
|
|
assert.ElementsMatch(t,
|
|
rowIDsOf(validateJob, pushJob, verifyInnerJob, smokeTestJob, cleanupJob, finishDeployJob),
|
|
plan.collectResetCallerDescendants().Values())
|
|
})
|
|
})
|
|
|
|
t.Run("CollectResetCallerDescendants", func(t *testing.T) {
|
|
planWith := func(rerunJobs ...*actions_model.ActionRunJob) *rerunPlan {
|
|
set := make(container.Set[int64])
|
|
for _, j := range rerunJobs {
|
|
set.Add(j.AttemptJobID)
|
|
}
|
|
return &rerunPlan{templateJobs: jobs, rerunAttemptJobIDs: set}
|
|
}
|
|
|
|
t.Run("non-caller in reset set is ignored", func(t *testing.T) {
|
|
assert.Empty(t, planWith(smokeTestJob).collectResetCallerDescendants())
|
|
})
|
|
|
|
t.Run("caller in reset set returns transitive descendants", func(t *testing.T) {
|
|
out := planWith(deployJob).collectResetCallerDescendants()
|
|
assert.ElementsMatch(t,
|
|
rowIDsOf(validateJob, pushJob, verifyInnerJob, smokeTestJob, cleanupJob, finishDeployJob),
|
|
out.Values())
|
|
})
|
|
|
|
t.Run("multiple reset callers union their descendants", func(t *testing.T) {
|
|
out := planWith(deployJob, verifyInnerJob).collectResetCallerDescendants()
|
|
assert.ElementsMatch(t,
|
|
rowIDsOf(validateJob, pushJob, verifyInnerJob, smokeTestJob, cleanupJob, finishDeployJob),
|
|
out.Values())
|
|
})
|
|
|
|
t.Run("nested-only reset returns just the nested subtree", func(t *testing.T) {
|
|
out := planWith(verifyInnerJob).collectResetCallerDescendants()
|
|
assert.ElementsMatch(t, rowIDsOf(smokeTestJob, cleanupJob), out.Values())
|
|
})
|
|
})
|
|
|
|
t.Run("HasRerunDependency", func(t *testing.T) {
|
|
t.Run("no needs returns false", func(t *testing.T) {
|
|
plan := &rerunPlan{
|
|
templateJobs: []*actions_model.ActionRunJob{buildJob},
|
|
rerunAttemptJobIDs: make(container.Set[int64]),
|
|
ancestorAttemptJobIDs: make(container.Set[int64]),
|
|
}
|
|
assert.False(t, plan.hasRerunDependency(buildJob))
|
|
})
|
|
|
|
t.Run("dependency in rerun set returns true", func(t *testing.T) {
|
|
plan := &rerunPlan{
|
|
templateJobs: jobs,
|
|
rerunAttemptJobIDs: container.SetOf(smokeTestJob.AttemptJobID),
|
|
ancestorAttemptJobIDs: make(container.Set[int64]),
|
|
}
|
|
// cleanup `needs: [smoke-test]`, both in inner verify scope.
|
|
assert.True(t, plan.hasRerunDependency(cleanupJob))
|
|
})
|
|
|
|
t.Run("dependency in ancestor set returns true", func(t *testing.T) {
|
|
plan := &rerunPlan{
|
|
templateJobs: jobs,
|
|
rerunAttemptJobIDs: container.SetOf(attemptJobIDsOf(smokeTestJob, cleanupJob)...),
|
|
ancestorAttemptJobIDs: container.SetOf(verifyInnerJob.AttemptJobID),
|
|
}
|
|
assert.True(t, plan.hasRerunDependency(finishDeployJob))
|
|
})
|
|
|
|
t.Run("dependency on unrelated sibling returns false", func(t *testing.T) {
|
|
plan := &rerunPlan{
|
|
templateJobs: jobs,
|
|
rerunAttemptJobIDs: container.SetOf(smokeTestJob.AttemptJobID),
|
|
ancestorAttemptJobIDs: make(container.Set[int64]),
|
|
}
|
|
assert.False(t, plan.hasRerunDependency(pushJob))
|
|
})
|
|
|
|
t.Run("scope-bound: same JobID in another scope does not match", func(t *testing.T) {
|
|
plan := &rerunPlan{
|
|
templateJobs: jobs,
|
|
rerunAttemptJobIDs: container.SetOf(verifyTopJob.AttemptJobID),
|
|
ancestorAttemptJobIDs: make(container.Set[int64]),
|
|
}
|
|
assert.False(t, plan.hasRerunDependency(finishDeployJob))
|
|
|
|
// Sanity: swap to the inner verify and the same target now sees it.
|
|
plan.rerunAttemptJobIDs = container.SetOf(verifyInnerJob.AttemptJobID)
|
|
assert.True(t, plan.hasRerunDependency(finishDeployJob))
|
|
})
|
|
})
|
|
}
|
|
|
|
// templateJob is a small constructor for fixture jobs used by the rerunPlan unit tests.
|
|
func templateJob(id, attemptJobID int64, jobID string, parentID int64, isCaller bool, needs ...string) *actions_model.ActionRunJob {
|
|
return &actions_model.ActionRunJob{
|
|
ID: id,
|
|
AttemptJobID: attemptJobID,
|
|
JobID: jobID,
|
|
ParentJobID: parentID,
|
|
IsReusableCaller: isCaller,
|
|
Needs: needs,
|
|
}
|
|
}
|
|
|
|
func attemptJobIDsOf(jobs ...*actions_model.ActionRunJob) []int64 {
|
|
out := make([]int64, len(jobs))
|
|
for i, j := range jobs {
|
|
out[i] = j.AttemptJobID
|
|
}
|
|
return out
|
|
}
|
|
|
|
func rowIDsOf(jobs ...*actions_model.ActionRunJob) []int64 {
|
|
out := make([]int64, len(jobs))
|
|
for i, j := range jobs {
|
|
out[i] = j.ID
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestCollectMatrixCollapse(t *testing.T) {
|
|
// A dynamic-matrix group is the anchor (the combination that kept DeferredMatrixPayload) plus its
|
|
// sibling combinations. Collapsing it rewinds the anchor to an unexpanded placeholder and drops
|
|
// the siblings, so the new attempt re-derives the matrix from the fresh needs outputs.
|
|
matrixRow := func(id, attemptJobID int64, jobID string, parentID int64, isAnchor, isCaller bool, needs ...string) *actions_model.ActionRunJob {
|
|
job := templateJob(id, attemptJobID, jobID, parentID, isCaller, needs...)
|
|
if isAnchor {
|
|
job.DeferredMatrixPayload = []byte("name: t\non: push\njobs:\n build:\n steps: [{run: echo}]\n")
|
|
}
|
|
return job
|
|
}
|
|
|
|
t.Run("a rerun of the needs collapses the group", func(t *testing.T) {
|
|
generate := templateJob(101, 1, "generate", 0, false)
|
|
build1 := matrixRow(102, 2, "build", 0, true, false, "generate")
|
|
build2 := matrixRow(103, 3, "build", 0, false, false, "generate")
|
|
plan := &rerunPlan{templateJobs: []*actions_model.ActionRunJob{generate, build1, build2}}
|
|
require.NoError(t, plan.expandRerunJobIDs([]*actions_model.ActionRunJob{generate}))
|
|
plan.skipCloneTemplateJobIDs = plan.collectResetCallerDescendants()
|
|
plan.collectMatrixCollapse()
|
|
|
|
assert.ElementsMatch(t, rowIDsOf(build1), plan.matrixPlaceholderTemplateIDs.Values())
|
|
assert.ElementsMatch(t, rowIDsOf(build2), plan.matrixSiblingSkipTemplateIDs.Values())
|
|
})
|
|
|
|
t.Run("re-running only a combination keeps the group as it is", func(t *testing.T) {
|
|
// generate is not re-run, so its outputs still stand and the combinations stay valid.
|
|
generate := templateJob(101, 1, "generate", 0, false)
|
|
build1 := matrixRow(102, 2, "build", 0, true, false, "generate")
|
|
build2 := matrixRow(103, 3, "build", 0, false, false, "generate")
|
|
plan := &rerunPlan{templateJobs: []*actions_model.ActionRunJob{generate, build1, build2}}
|
|
require.NoError(t, plan.expandRerunJobIDs([]*actions_model.ActionRunJob{build2}))
|
|
plan.skipCloneTemplateJobIDs = plan.collectResetCallerDescendants()
|
|
plan.collectMatrixCollapse()
|
|
|
|
assert.Empty(t, plan.matrixPlaceholderTemplateIDs)
|
|
assert.Empty(t, plan.matrixSiblingSkipTemplateIDs)
|
|
})
|
|
|
|
t.Run("a pass-through anchor is never rewound", func(t *testing.T) {
|
|
// build is a matrix-expanded reusable caller. Re-running a job inside build (1)'s subtree
|
|
// together with generate leaves build (1) an ancestor rather than a rerun job, while build (2)
|
|
// does join the rerun set. execRerunPlan clones an ancestor with its old terminal status, so
|
|
// restoring the placeholder onto it would leave a done job holding the raw, unexpanded payload
|
|
// that nothing ever expands - and drop build (2) on top of it.
|
|
generate := templateJob(101, 1, "generate", 0, false)
|
|
build1 := matrixRow(102, 2, "build", 0, true, true, "generate")
|
|
build2 := matrixRow(103, 3, "build", 0, false, true, "generate")
|
|
inner := templateJob(104, 4, "inner", 102, false)
|
|
plan := &rerunPlan{templateJobs: []*actions_model.ActionRunJob{generate, build1, build2, inner}}
|
|
require.NoError(t, plan.expandRerunJobIDs([]*actions_model.ActionRunJob{inner, generate}))
|
|
plan.skipCloneTemplateJobIDs = plan.collectResetCallerDescendants()
|
|
|
|
require.Contains(t, plan.ancestorAttemptJobIDs, build1.AttemptJobID)
|
|
require.NotContains(t, plan.rerunAttemptJobIDs, build1.AttemptJobID)
|
|
require.Contains(t, plan.rerunAttemptJobIDs, build2.AttemptJobID)
|
|
|
|
plan.collectMatrixCollapse()
|
|
assert.Empty(t, plan.matrixPlaceholderTemplateIDs)
|
|
assert.Empty(t, plan.matrixSiblingSkipTemplateIDs)
|
|
})
|
|
|
|
t.Run("an unexpanded placeholder is rewound whenever it is re-run", func(t *testing.T) {
|
|
// The previous attempt never got to expand it, so there is nothing to reuse.
|
|
generate := templateJob(101, 1, "generate", 0, false)
|
|
build := matrixRow(102, 2, "build", 0, true, false, "generate")
|
|
build.IsMatrixDeferred = true
|
|
plan := &rerunPlan{templateJobs: []*actions_model.ActionRunJob{generate, build}}
|
|
require.NoError(t, plan.expandRerunJobIDs([]*actions_model.ActionRunJob{build}))
|
|
plan.skipCloneTemplateJobIDs = plan.collectResetCallerDescendants()
|
|
plan.collectMatrixCollapse()
|
|
|
|
assert.ElementsMatch(t, rowIDsOf(build), plan.matrixPlaceholderTemplateIDs.Values())
|
|
assert.Empty(t, plan.matrixSiblingSkipTemplateIDs)
|
|
})
|
|
|
|
t.Run("a plan-time matrix is left alone", func(t *testing.T) {
|
|
generate := templateJob(101, 1, "generate", 0, false)
|
|
build1 := matrixRow(102, 2, "build", 0, false, false, "generate")
|
|
build2 := matrixRow(103, 3, "build", 0, false, false, "generate")
|
|
plan := &rerunPlan{templateJobs: []*actions_model.ActionRunJob{generate, build1, build2}}
|
|
require.NoError(t, plan.expandRerunJobIDs(nil))
|
|
plan.skipCloneTemplateJobIDs = plan.collectResetCallerDescendants()
|
|
plan.collectMatrixCollapse()
|
|
|
|
assert.Empty(t, plan.matrixPlaceholderTemplateIDs)
|
|
assert.Empty(t, plan.matrixSiblingSkipTemplateIDs)
|
|
})
|
|
}
|