agola/internal/services/runservice/scheduler/scheduler_test.go

542 lines
16 KiB
Go

// Copyright 2019 Sorint.lab
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied
// See the License for the specific language governing permissions and
// limitations under the License.
package scheduler
import (
"context"
"sort"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/sorintlab/agola/internal/services/runservice/types"
)
func TestAdvanceRunTasks(t *testing.T) {
// a global run config for all tests
rc := &types.RunConfig{
Tasks: map[string]*types.RunConfigTask{
"task01": &types.RunConfigTask{
ID: "task01",
Name: "task01",
Depends: map[string]*types.RunConfigTaskDepend{},
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: []interface{}{},
Skip: false,
},
"task02": &types.RunConfigTask{
ID: "task02",
Name: "task02",
Depends: map[string]*types.RunConfigTaskDepend{
"task01": &types.RunConfigTaskDepend{TaskID: "task01", Conditions: []types.RunConfigTaskDependCondition{types.RunConfigTaskDependConditionOnSuccess}},
},
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: []interface{}{},
Skip: false,
},
"task03": &types.RunConfigTask{
ID: "task03",
Name: "task03",
Depends: map[string]*types.RunConfigTaskDepend{},
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: []interface{}{},
Skip: false,
},
"task04": &types.RunConfigTask{
ID: "task04",
Name: "task04",
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: []interface{}{},
Skip: false,
},
"task05": &types.RunConfigTask{
ID: "task05",
Name: "task05",
Depends: map[string]*types.RunConfigTaskDepend{
"task03": &types.RunConfigTaskDepend{TaskID: "task03", Conditions: []types.RunConfigTaskDependCondition{types.RunConfigTaskDependConditionOnSuccess}},
"task04": &types.RunConfigTaskDepend{TaskID: "task04", Conditions: []types.RunConfigTaskDependCondition{types.RunConfigTaskDependConditionOnSuccess}},
},
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: []interface{}{},
Skip: false,
},
},
}
// initial run that matched the runconfig:
// * the run is in phase running with result unknown
// * all tasks are not started or skipped
// (if the runconfig task as Skip == true). This must match the status
// generated by command.genRun()
run := &types.Run{
Phase: types.RunPhaseRunning,
Result: types.RunResultUnknown,
Tasks: map[string]*types.RunTask{
"task01": &types.RunTask{
ID: "task01",
Status: types.RunTaskStatusNotStarted,
},
"task02": &types.RunTask{
ID: "task02",
Status: types.RunTaskStatusNotStarted,
},
"task03": &types.RunTask{
ID: "task03",
Status: types.RunTaskStatusNotStarted,
},
"task04": &types.RunTask{
ID: "task04",
Status: types.RunTaskStatusNotStarted,
},
"task05": &types.RunTask{
ID: "task05",
Status: types.RunTaskStatusNotStarted,
},
},
}
tests := []struct {
name string
rc *types.RunConfig
r *types.Run
activeExecutorTasks []*types.ExecutorTask
out *types.Run
err error
}{
{
name: "test top level task not started",
rc: rc,
r: run.DeepCopy(),
out: run.DeepCopy(),
},
{
name: "test task status set to skipped when parent status is skipped",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task01"].Skip = true
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task01"].Status = types.RunTaskStatusSkipped
return run
}(),
out: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task01"].Status = types.RunTaskStatusSkipped
run.Tasks["task02"].Status = types.RunTaskStatusSkipped
return run
}(),
},
{
name: "test task status set to skipped when all parent status is skipped",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task03"].Skip = true
rc.Tasks["task04"].Skip = true
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSkipped
return run
}(),
out: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSkipped
run.Tasks["task05"].Status = types.RunTaskStatusSkipped
return run
}(),
},
{
name: "test task set to skipped when only some parents status is skipped",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task03"].Skip = true
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
return run
}(),
out: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
run.Tasks["task05"].Status = types.RunTaskStatusSkipped
return run
}(),
},
{
name: "test task set to skipped when one of the parents doesn't match default conditions (on_success)",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task03"].Skip = true
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
return run
}(),
out: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
run.Tasks["task05"].Status = types.RunTaskStatusSkipped
return run
}(),
},
{
name: "test task set to skipped when one of the parents doesn't match custom conditions",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task03"].Skip = true
rc.Tasks["task05"].Depends["task03"].Conditions = []types.RunConfigTaskDependCondition{types.RunConfigTaskDependConditionOnFailure}
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
return run
}(),
out: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
run.Tasks["task05"].Status = types.RunTaskStatusSkipped
return run
}(),
},
{
name: "test task set to not skipped when one of the parent is skipped and task condition is on_skipped",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task03"].Skip = true
rc.Tasks["task05"].Depends["task03"].Conditions = []types.RunConfigTaskDependCondition{types.RunConfigTaskDependConditionOnSkipped}
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
return run
}(),
out: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
return run
}(),
},
{
name: "test task not set to waiting approval when task is skipped",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task03"].Skip = true
rc.Tasks["task05"].NeedsApproval = true
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
return run
}(),
out: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
run.Tasks["task05"].Status = types.RunTaskStatusSkipped
return run
}(),
},
{
name: "test task set to waiting approval when all the parents are finished and task is not skipped",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task03"].Skip = true
rc.Tasks["task05"].NeedsApproval = true
rc.Tasks["task05"].Depends["task03"].Conditions = []types.RunConfigTaskDependCondition{types.RunConfigTaskDependConditionOnSkipped}
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
return run
}(),
out: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task03"].Status = types.RunTaskStatusSkipped
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
run.Tasks["task05"].WaitingApproval = true
return run
}(),
},
{
name: "cancel all root not started tasks when run has a result set",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Result = types.RunResultSuccess
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
return run
}(),
out: func() *types.Run {
run := run.DeepCopy()
run.Result = types.RunResultSuccess
run.Tasks["task01"].Status = types.RunTaskStatusCancelled
run.Tasks["task02"].Status = types.RunTaskStatusNotStarted
run.Tasks["task03"].Status = types.RunTaskStatusCancelled
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
run.Tasks["task05"].Status = types.RunTaskStatusNotStarted
return run
}(),
},
{
name: "cancel all root not started tasks when run has a result set (task01 is already scheduled)",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Result = types.RunResultSuccess
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
return run
}(),
activeExecutorTasks: []*types.ExecutorTask{
&types.ExecutorTask{ID: "task01"},
},
out: func() *types.Run {
run := run.DeepCopy()
run.Result = types.RunResultSuccess
run.Tasks["task01"].Status = types.RunTaskStatusNotStarted
run.Tasks["task02"].Status = types.RunTaskStatusNotStarted
run.Tasks["task03"].Status = types.RunTaskStatusCancelled
run.Tasks["task04"].Status = types.RunTaskStatusSuccess
run.Tasks["task05"].Status = types.RunTaskStatusNotStarted
return run
}(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
r, err := advanceRunTasks(ctx, tt.r, tt.rc, tt.activeExecutorTasks)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if diff := cmp.Diff(tt.out, r); diff != "" {
t.Error(diff)
}
})
}
}
func TestGetTasksToRun(t *testing.T) {
// a global run config for all tests
rc := &types.RunConfig{
Tasks: map[string]*types.RunConfigTask{
"task01": &types.RunConfigTask{
ID: "task01",
Name: "task01",
Depends: map[string]*types.RunConfigTaskDepend{},
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: []interface{}{},
Skip: false,
},
"task02": &types.RunConfigTask{
ID: "task02",
Name: "task02",
Depends: map[string]*types.RunConfigTaskDepend{
"task01": &types.RunConfigTaskDepend{TaskID: "task01", Conditions: []types.RunConfigTaskDependCondition{types.RunConfigTaskDependConditionOnSuccess}},
},
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: []interface{}{},
Skip: false,
},
"task03": &types.RunConfigTask{
ID: "task03",
Name: "task03",
Depends: map[string]*types.RunConfigTaskDepend{},
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: []interface{}{},
Skip: false,
},
"task04": &types.RunConfigTask{
ID: "task04",
Name: "task04",
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: []interface{}{},
Skip: false,
},
"task05": &types.RunConfigTask{
ID: "task05",
Name: "task05",
Depends: map[string]*types.RunConfigTaskDepend{
"task03": &types.RunConfigTaskDepend{TaskID: "task03", Conditions: []types.RunConfigTaskDependCondition{types.RunConfigTaskDependConditionOnSuccess}},
"task04": &types.RunConfigTaskDepend{TaskID: "task04", Conditions: []types.RunConfigTaskDependCondition{types.RunConfigTaskDependConditionOnSuccess}},
},
Runtime: &types.Runtime{Type: types.RuntimeType("pod"),
Containers: []*types.Container{{Image: "image01"}},
},
Environment: map[string]string{},
Steps: []interface{}{},
Skip: false,
},
},
}
// initial run that matched the runconfig, all tasks are not started or skipped
// (if the runconfig task as Skip == true). This must match the status
// generated by command.genRun()
run := &types.Run{
Tasks: map[string]*types.RunTask{
"task01": &types.RunTask{
ID: "task01",
Status: types.RunTaskStatusNotStarted,
},
"task02": &types.RunTask{
ID: "task02",
Status: types.RunTaskStatusNotStarted,
},
"task03": &types.RunTask{
ID: "task03",
Status: types.RunTaskStatusNotStarted,
},
"task04": &types.RunTask{
ID: "task04",
Status: types.RunTaskStatusNotStarted,
},
"task05": &types.RunTask{
ID: "task05",
Status: types.RunTaskStatusNotStarted,
},
},
}
tests := []struct {
name string
rc *types.RunConfig
r *types.Run
out []string
err error
}{
{
name: "test run top level tasks",
rc: rc,
r: run.DeepCopy(),
out: []string{"task01", "task03", "task04"},
},
{
name: "test don't run skipped tasks",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task01"].Skip = true
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task01"].Status = types.RunTaskStatusSkipped
run.Tasks["task02"].Status = types.RunTaskStatusSkipped
return run
}(),
out: []string{"task03", "task04"},
},
{
name: "test don't run if needs approval but not approved",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task01"].NeedsApproval = true
return rc
}(),
r: run.DeepCopy(),
out: []string{"task03", "task04"},
},
{
name: "test run if needs approval and approved",
rc: func() *types.RunConfig {
rc := rc.DeepCopy()
rc.Tasks["task01"].NeedsApproval = true
return rc
}(),
r: func() *types.Run {
run := run.DeepCopy()
run.Tasks["task01"].Approved = true
return run
}(),
out: []string{"task01", "task03", "task04"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
tasks, err := getTasksToRun(ctx, tt.r, tt.rc)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
outTasks := []string{}
for _, t := range tasks {
outTasks = append(outTasks, t.ID)
}
sort.Sort(sort.StringSlice(tt.out))
sort.Sort(sort.StringSlice(outTasks))
if diff := cmp.Diff(tt.out, outTasks); diff != "" {
t.Error(diff)
}
})
}
}