// 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 runservice import ( "context" "sort" "testing" "time" "agola.io/agola/internal/testutil" "agola.io/agola/services/runservice/types" ctypes "agola.io/agola/services/types" "github.com/google/go-cmp/cmp" ) 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: types.Steps{}, 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: types.Steps{}, 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: types.Steps{}, Skip: false, }, "task04": &types.RunConfigTask{ ID: "task04", Name: "task04", Depends: map[string]*types.RunConfigTaskDepend{}, Runtime: &types.Runtime{Type: types.RuntimeType("pod"), Containers: []*types.Container{{Image: "image01"}}, }, Environment: map[string]string{}, Steps: types.Steps{}, 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: types.Steps{}, 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 scheduledExecutorTasks []*types.ExecutorTask out *types.Run }{ { 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 }(), scheduledExecutorTasks: []*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 }(), }, { name: "skip all not started tasks when run is set to stop", rc: func() *types.RunConfig { rc := rc.DeepCopy() return rc }(), r: func() *types.Run { run := run.DeepCopy() run.Tasks["task01"].Status = types.RunTaskStatusRunning run.Tasks["task04"].Status = types.RunTaskStatusSuccess run.Tasks["task03"].Status = types.RunTaskStatusCancelled run.Stop = true return run }(), scheduledExecutorTasks: []*types.ExecutorTask{ &types.ExecutorTask{ID: "task01"}, }, out: func() *types.Run { run := run.DeepCopy() run.Stop = true run.Tasks["task01"].Status = types.RunTaskStatusRunning run.Tasks["task02"].Status = types.RunTaskStatusSkipped run.Tasks["task03"].Status = types.RunTaskStatusCancelled run.Tasks["task04"].Status = types.RunTaskStatusSuccess run.Tasks["task05"].Status = types.RunTaskStatusSkipped return run }(), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() log := testutil.NewLogger(t) r, err := advanceRunTasks(ctx, log, tt.r, tt.rc, tt.scheduledExecutorTasks) 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: types.Steps{}, 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: types.Steps{}, 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: types.Steps{}, 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: types.Steps{}, 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: types.Steps{}, 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 }{ { 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() log := testutil.NewLogger(t) tasks, err := getTasksToRun(ctx, log, 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.Strings(tt.out) sort.Strings(outTasks) if diff := cmp.Diff(tt.out, outTasks); diff != "" { t.Error(diff) } }) } } func TestChooseExecutor(t *testing.T) { executorOK := &types.Executor{ ID: "executorOK", Archs: []ctypes.Arch{ctypes.ArchAMD64}, ActiveTasksLimit: 2, ActiveTasks: 0, LastStatusUpdateTime: time.Now(), } executorNoFreeTaskSlots := func() *types.Executor { e := executorOK.DeepCopy() e.ID = "executorNoFreeTasksSlots" e.ActiveTasks = 2 return e }() executorNotAlive := func() *types.Executor { e := executorOK.DeepCopy() e.ID = "executorNotAlive" e.LastStatusUpdateTime = time.Now().Add(-120 * time.Second) return e }() executorOKMultipleArchs := func() *types.Executor { e := executorOK.DeepCopy() e.ID = "executorOKMultipleArchs" e.Archs = []ctypes.Arch{ctypes.ArchAMD64, ctypes.ArchARM64} return e }() executorOKAllowsPriviledContainers := func() *types.Executor { e := executorOK.DeepCopy() e.ID = "executorOKAllowsPrivilegedContainers" e.AllowPrivilegedContainers = true return e }() // Only primary and the required variables for this test are set rct := &types.RunConfigTask{ ID: "task01", Name: "task01", Runtime: &types.Runtime{Type: types.RuntimeType("pod"), Arch: ctypes.ArchAMD64, }, } rctWithPrivilegedContainers := &types.RunConfigTask{ ID: "task01", Name: "task01", Runtime: &types.Runtime{Type: types.RuntimeType("pod"), Arch: ctypes.ArchAMD64, Containers: []*types.Container{ { Privileged: true, }, }, }, } tests := []struct { name string executors []*types.Executor rct *types.RunConfigTask out *types.Executor }{ { name: "test single executor ok", executors: []*types.Executor{executorOK}, // Only primary and the required variables for this test are set rct: rct, out: executorOK, }, { name: "test single executor without free task slots", executors: []*types.Executor{executorNoFreeTaskSlots}, // Only primary and the required variables for this test are set rct: rct, out: nil, }, { name: "test single executor not alive", executors: []*types.Executor{executorNotAlive}, rct: rct, out: nil, }, { name: "test single executor with different arch", executors: func() []*types.Executor { e := executorOK.DeepCopy() e.Archs = []ctypes.Arch{ctypes.ArchARM64} return []*types.Executor{e} }(), rct: rct, out: nil, }, { name: "test single executor with multiple archs and one matches the task required arch", executors: []*types.Executor{executorOKMultipleArchs}, rct: rct, out: executorOKMultipleArchs, }, { name: "test single executor without allowed privileged container but privileged containers are required", executors: []*types.Executor{executorOK}, rct: rctWithPrivilegedContainers, out: nil, }, { name: "test single executor with allowed privileged container and privileged containers are required", executors: []*types.Executor{executorOKAllowsPriviledContainers}, rct: rctWithPrivilegedContainers, out: executorOKAllowsPriviledContainers, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { e := chooseExecutor(tt.executors, map[string]int{}, tt.rct) if e == nil && tt.out == nil { return } if e == nil && tt.out != nil { t.Fatalf("expected executor with id: %s, go no executor selected", tt.out.ID) } if e != nil && tt.out == nil { t.Fatalf("expected no executor selected, got executor with id: %s", e.ID) } if e != tt.out { t.Fatalf("wrong executor ID, expected %s, got: %s", tt.out.ID, e.ID) } }) } }