package progress import ( "fmt" "strconv" "testing" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/pkg/progress" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) type mockProgress struct { p []progress.Progress } func (mp *mockProgress) WriteProgress(p progress.Progress) error { mp.p = append(mp.p, p) return nil } func (mp *mockProgress) clear() { mp.p = nil } type updaterTester struct { t *testing.T updater progressUpdater p *mockProgress service swarm.Service activeNodes map[string]struct{} rollback bool } func (u updaterTester) testUpdater(tasks []swarm.Task, expectedConvergence bool, expectedProgress []progress.Progress) { u.p.clear() converged, err := u.updater.update(u.service, tasks, u.activeNodes, u.rollback) assert.Check(u.t, err) assert.Check(u.t, is.Equal(expectedConvergence, converged)) assert.Check(u.t, is.DeepEqual(expectedProgress, u.p.p)) } func (u updaterTester) testUpdaterNoOrder(tasks []swarm.Task, expectedConvergence bool, expectedProgress []progress.Progress) { u.p.clear() converged, err := u.updater.update(u.service, tasks, u.activeNodes, u.rollback) assert.Check(u.t, err) assert.Check(u.t, is.Equal(expectedConvergence, converged)) // instead of checking that expected and actual match exactly, verify that // they are the same length, and every time from actual is in expected. assert.Check(u.t, is.Equal(len(expectedProgress), len(u.p.p))) for _, prog := range expectedProgress { assert.Check(u.t, is.Contains(u.p.p, prog)) } } func TestReplicatedProgressUpdaterOneReplica(t *testing.T) { replicas := uint64(1) service := swarm.Service{ Spec: swarm.ServiceSpec{ Mode: swarm.ServiceMode{ Replicated: &swarm.ReplicatedService{ Replicas: &replicas, }, }, }, } p := &mockProgress{} updaterTester := updaterTester{ t: t, updater: &replicatedProgressUpdater{ progressOut: p, }, p: p, activeNodes: map[string]struct{}{"a": {}, "b": {}}, service: service, } tasks := []swarm.Task{} updaterTester.testUpdater(tasks, false, []progress.Progress{ {ID: "overall progress", Action: "0 out of 1 tasks"}, {ID: "1/1", Action: " "}, {ID: "overall progress", Action: "0 out of 1 tasks"}, }) // Task with DesiredState beyond Running is ignored tasks = append(tasks, swarm.Task{ID: "1", NodeID: "a", DesiredState: swarm.TaskStateShutdown, Status: swarm.TaskStatus{State: swarm.TaskStateNew}, }) updaterTester.testUpdater(tasks, false, []progress.Progress{ {ID: "overall progress", Action: "0 out of 1 tasks"}, }) // Task with valid DesiredState and State updates progress bar tasks[0].DesiredState = swarm.TaskStateRunning updaterTester.testUpdater(tasks, false, []progress.Progress{ {ID: "1/1", Action: "new ", Current: 1, Total: 9, HideCounts: true}, {ID: "overall progress", Action: "0 out of 1 tasks"}, }) // If the task exposes an error, we should show that instead of the // progress bar. tasks[0].Status.Err = "something is wrong" updaterTester.testUpdater(tasks, false, []progress.Progress{ {ID: "1/1", Action: "something is wrong"}, {ID: "overall progress", Action: "0 out of 1 tasks"}, }) // When the task reaches running, update should return true tasks[0].Status.Err = "" tasks[0].Status.State = swarm.TaskStateRunning updaterTester.testUpdater(tasks, true, []progress.Progress{ {ID: "1/1", Action: "running ", Current: 9, Total: 9, HideCounts: true}, {ID: "overall progress", Action: "1 out of 1 tasks"}, }) // If the task fails, update should return false again tasks[0].Status.Err = "task failed" tasks[0].Status.State = swarm.TaskStateFailed updaterTester.testUpdater(tasks, false, []progress.Progress{ {ID: "1/1", Action: "task failed"}, {ID: "overall progress", Action: "0 out of 1 tasks"}, }) // If the task is restarted, progress output should be shown for the // replacement task, not the old task. tasks[0].DesiredState = swarm.TaskStateShutdown tasks = append(tasks, swarm.Task{ID: "2", NodeID: "b", DesiredState: swarm.TaskStateRunning, Status: swarm.TaskStatus{State: swarm.TaskStateRunning}, }) updaterTester.testUpdater(tasks, true, []progress.Progress{ {ID: "1/1", Action: "running ", Current: 9, Total: 9, HideCounts: true}, {ID: "overall progress", Action: "1 out of 1 tasks"}, }) // Add a new task while the current one is still running, to simulate // "start-then-stop" updates. tasks = append(tasks, swarm.Task{ID: "3", NodeID: "b", DesiredState: swarm.TaskStateRunning, Status: swarm.TaskStatus{State: swarm.TaskStatePreparing}, }) updaterTester.testUpdater(tasks, false, []progress.Progress{ {ID: "1/1", Action: "preparing", Current: 6, Total: 9, HideCounts: true}, {ID: "overall progress", Action: "0 out of 1 tasks"}, }) } func TestReplicatedProgressUpdaterManyReplicas(t *testing.T) { replicas := uint64(50) service := swarm.Service{ Spec: swarm.ServiceSpec{ Mode: swarm.ServiceMode{ Replicated: &swarm.ReplicatedService{ Replicas: &replicas, }, }, }, } p := &mockProgress{} updaterTester := updaterTester{ t: t, updater: &replicatedProgressUpdater{ progressOut: p, }, p: p, activeNodes: map[string]struct{}{"a": {}, "b": {}}, service: service, } tasks := []swarm.Task{} // No per-task progress bars because there are too many replicas updaterTester.testUpdater(tasks, false, []progress.Progress{ {ID: "overall progress", Action: fmt.Sprintf("0 out of %d tasks", replicas)}, {ID: "overall progress", Action: fmt.Sprintf("0 out of %d tasks", replicas)}, }) for i := 0; i != int(replicas); i++ { tasks = append(tasks, swarm.Task{ ID: strconv.Itoa(i), Slot: i + 1, NodeID: "a", DesiredState: swarm.TaskStateRunning, Status: swarm.TaskStatus{State: swarm.TaskStateNew}, }) if i%2 == 1 { tasks[i].NodeID = "b" } updaterTester.testUpdater(tasks, false, []progress.Progress{ {ID: "overall progress", Action: fmt.Sprintf("%d out of %d tasks", i, replicas)}, }) tasks[i].Status.State = swarm.TaskStateRunning updaterTester.testUpdater(tasks, uint64(i) == replicas-1, []progress.Progress{ {ID: "overall progress", Action: fmt.Sprintf("%d out of %d tasks", i+1, replicas)}, }) } } func TestGlobalProgressUpdaterOneNode(t *testing.T) { service := swarm.Service{ Spec: swarm.ServiceSpec{ Mode: swarm.ServiceMode{ Global: &swarm.GlobalService{}, }, }, } p := &mockProgress{} updaterTester := updaterTester{ t: t, updater: &globalProgressUpdater{ progressOut: p, }, p: p, activeNodes: map[string]struct{}{"a": {}, "b": {}}, service: service, } tasks := []swarm.Task{} updaterTester.testUpdater(tasks, false, []progress.Progress{ {ID: "overall progress", Action: "waiting for new tasks"}, }) // Task with DesiredState beyond Running is ignored tasks = append(tasks, swarm.Task{ID: "1", NodeID: "a", DesiredState: swarm.TaskStateShutdown, Status: swarm.TaskStatus{State: swarm.TaskStateNew}, }) updaterTester.testUpdater(tasks, false, []progress.Progress{ {ID: "overall progress", Action: "0 out of 1 tasks"}, {ID: "overall progress", Action: "0 out of 1 tasks"}, }) // Task with valid DesiredState and State updates progress bar tasks[0].DesiredState = swarm.TaskStateRunning updaterTester.testUpdater(tasks, false, []progress.Progress{ {ID: "a", Action: "new ", Current: 1, Total: 9, HideCounts: true}, {ID: "overall progress", Action: "0 out of 1 tasks"}, }) // If the task exposes an error, we should show that instead of the // progress bar. tasks[0].Status.Err = "something is wrong" updaterTester.testUpdater(tasks, false, []progress.Progress{ {ID: "a", Action: "something is wrong"}, {ID: "overall progress", Action: "0 out of 1 tasks"}, }) // When the task reaches running, update should return true tasks[0].Status.Err = "" tasks[0].Status.State = swarm.TaskStateRunning updaterTester.testUpdater(tasks, true, []progress.Progress{ {ID: "a", Action: "running ", Current: 9, Total: 9, HideCounts: true}, {ID: "overall progress", Action: "1 out of 1 tasks"}, }) // If the task fails, update should return false again tasks[0].Status.Err = "task failed" tasks[0].Status.State = swarm.TaskStateFailed updaterTester.testUpdater(tasks, false, []progress.Progress{ {ID: "a", Action: "task failed"}, {ID: "overall progress", Action: "0 out of 1 tasks"}, }) // If the task is restarted, progress output should be shown for the // replacement task, not the old task. tasks[0].DesiredState = swarm.TaskStateShutdown tasks = append(tasks, swarm.Task{ID: "2", NodeID: "a", DesiredState: swarm.TaskStateRunning, Status: swarm.TaskStatus{State: swarm.TaskStateRunning}, }) updaterTester.testUpdater(tasks, true, []progress.Progress{ {ID: "a", Action: "running ", Current: 9, Total: 9, HideCounts: true}, {ID: "overall progress", Action: "1 out of 1 tasks"}, }) // Add a new task while the current one is still running, to simulate // "start-then-stop" updates. tasks = append(tasks, swarm.Task{ID: "3", NodeID: "a", DesiredState: swarm.TaskStateRunning, Status: swarm.TaskStatus{State: swarm.TaskStatePreparing}, }) updaterTester.testUpdater(tasks, false, []progress.Progress{ {ID: "a", Action: "preparing", Current: 6, Total: 9, HideCounts: true}, {ID: "overall progress", Action: "0 out of 1 tasks"}, }) } func TestGlobalProgressUpdaterManyNodes(t *testing.T) { nodes := 50 service := swarm.Service{ Spec: swarm.ServiceSpec{ Mode: swarm.ServiceMode{ Global: &swarm.GlobalService{}, }, }, } p := &mockProgress{} updaterTester := updaterTester{ t: t, updater: &globalProgressUpdater{ progressOut: p, }, p: p, activeNodes: map[string]struct{}{}, service: service, } for i := 0; i != nodes; i++ { updaterTester.activeNodes[strconv.Itoa(i)] = struct{}{} } tasks := []swarm.Task{} updaterTester.testUpdater(tasks, false, []progress.Progress{ {ID: "overall progress", Action: "waiting for new tasks"}, }) for i := 0; i != nodes; i++ { tasks = append(tasks, swarm.Task{ ID: "task" + strconv.Itoa(i), NodeID: strconv.Itoa(i), DesiredState: swarm.TaskStateRunning, Status: swarm.TaskStatus{State: swarm.TaskStateNew}, }) } updaterTester.testUpdater(tasks, false, []progress.Progress{ {ID: "overall progress", Action: fmt.Sprintf("0 out of %d tasks", nodes)}, {ID: "overall progress", Action: fmt.Sprintf("0 out of %d tasks", nodes)}, }) for i := 0; i != nodes; i++ { tasks[i].Status.State = swarm.TaskStateRunning updaterTester.testUpdater(tasks, i == nodes-1, []progress.Progress{ {ID: "overall progress", Action: fmt.Sprintf("%d out of %d tasks", i+1, nodes)}, }) } } func TestReplicatedJobProgressUpdaterSmall(t *testing.T) { concurrent := uint64(2) total := uint64(5) service := swarm.Service{ Spec: swarm.ServiceSpec{ Mode: swarm.ServiceMode{ ReplicatedJob: &swarm.ReplicatedJob{ MaxConcurrent: &concurrent, TotalCompletions: &total, }, }, }, JobStatus: &swarm.JobStatus{ JobIteration: swarm.Version{Index: 1}, }, } p := &mockProgress{} ut := updaterTester{ t: t, updater: newReplicatedJobProgressUpdater(service, p), p: p, activeNodes: map[string]struct{}{"a": {}, "b": {}}, service: service, } // create some tasks belonging to a previous iteration tasks := []swarm.Task{ { ID: "oldtask1", Slot: 0, NodeID: "", DesiredState: swarm.TaskStateComplete, Status: swarm.TaskStatus{State: swarm.TaskStateNew}, JobIteration: &swarm.Version{Index: 0}, }, { ID: "oldtask2", Slot: 1, NodeID: "", DesiredState: swarm.TaskStateComplete, Status: swarm.TaskStatus{State: swarm.TaskStateComplete}, JobIteration: &swarm.Version{Index: 0}, }, } ut.testUpdater(tasks, false, []progress.Progress{ // on the initial pass, we draw all of the progress bars at once, which // puts them in order for the rest of the operation {ID: "job progress", Action: "0 out of 5 complete", Current: 0, Total: 5, HideCounts: true}, {ID: "active tasks", Action: "0 out of 2 tasks"}, {ID: "1/5", Action: " "}, {ID: "2/5", Action: " "}, {ID: "3/5", Action: " "}, {ID: "4/5", Action: " "}, {ID: "5/5", Action: " "}, // from here on, we draw as normal. as a side effect, we will have a // second update for the job progress and active tasks. This has no // practical effect on the UI, it's just a side effect of the update // logic. {ID: "job progress", Action: "0 out of 5 complete", Current: 0, Total: 5, HideCounts: true}, {ID: "active tasks", Action: "0 out of 2 tasks"}, }) // wipe the old tasks out of the list tasks = []swarm.Task{} tasks = append(tasks, swarm.Task{ ID: "task1", Slot: 0, NodeID: "", DesiredState: swarm.TaskStateComplete, Status: swarm.TaskStatus{State: swarm.TaskStateNew}, JobIteration: &swarm.Version{Index: service.JobStatus.JobIteration.Index}, }, swarm.Task{ ID: "task2", Slot: 1, NodeID: "", DesiredState: swarm.TaskStateComplete, Status: swarm.TaskStatus{State: swarm.TaskStateNew}, JobIteration: &swarm.Version{Index: service.JobStatus.JobIteration.Index}, }, ) ut.testUpdater(tasks, false, []progress.Progress{ {ID: "1/5", Action: "new ", Current: 1, Total: 10, HideCounts: true}, {ID: "2/5", Action: "new ", Current: 1, Total: 10, HideCounts: true}, {ID: "job progress", Action: "0 out of 5 complete", Current: 0, Total: 5, HideCounts: true}, {ID: "active tasks", Action: "2 out of 2 tasks"}, }) tasks[0].Status.State = swarm.TaskStatePreparing tasks[1].Status.State = swarm.TaskStateAssigned ut.testUpdater(tasks, false, []progress.Progress{ {ID: "1/5", Action: "preparing", Current: 6, Total: 10, HideCounts: true}, {ID: "2/5", Action: "assigned ", Current: 4, Total: 10, HideCounts: true}, {ID: "job progress", Action: "0 out of 5 complete", Current: 0, Total: 5, HideCounts: true}, {ID: "active tasks", Action: "2 out of 2 tasks"}, }) tasks[0].Status.State = swarm.TaskStateRunning tasks[1].Status.State = swarm.TaskStatePreparing ut.testUpdater(tasks, false, []progress.Progress{ {ID: "1/5", Action: "running ", Current: 9, Total: 10, HideCounts: true}, {ID: "2/5", Action: "preparing", Current: 6, Total: 10, HideCounts: true}, {ID: "job progress", Action: "0 out of 5 complete", Current: 0, Total: 5, HideCounts: true}, {ID: "active tasks", Action: "2 out of 2 tasks"}, }) tasks[0].Status.State = swarm.TaskStateComplete tasks[1].Status.State = swarm.TaskStateComplete ut.testUpdater(tasks, false, []progress.Progress{ {ID: "1/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "2/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "job progress", Action: "2 out of 5 complete", Current: 2, Total: 5, HideCounts: true}, {ID: "active tasks", Action: "0 out of 2 tasks"}, }) tasks = append(tasks, swarm.Task{ ID: "task3", Slot: 2, NodeID: "", DesiredState: swarm.TaskStateComplete, Status: swarm.TaskStatus{State: swarm.TaskStateNew}, JobIteration: &swarm.Version{Index: service.JobStatus.JobIteration.Index}, }, swarm.Task{ ID: "task4", Slot: 3, NodeID: "", DesiredState: swarm.TaskStateComplete, Status: swarm.TaskStatus{State: swarm.TaskStateNew}, JobIteration: &swarm.Version{Index: service.JobStatus.JobIteration.Index}, }, ) ut.testUpdater(tasks, false, []progress.Progress{ {ID: "1/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "2/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "3/5", Action: "new ", Current: 1, Total: 10, HideCounts: true}, {ID: "4/5", Action: "new ", Current: 1, Total: 10, HideCounts: true}, {ID: "job progress", Action: "2 out of 5 complete", Current: 2, Total: 5, HideCounts: true}, {ID: "active tasks", Action: "2 out of 2 tasks"}, }) tasks[2].Status.State = swarm.TaskStateRunning tasks[3].Status.State = swarm.TaskStateRunning ut.testUpdater(tasks, false, []progress.Progress{ {ID: "1/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "2/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "3/5", Action: "running ", Current: 9, Total: 10, HideCounts: true}, {ID: "4/5", Action: "running ", Current: 9, Total: 10, HideCounts: true}, {ID: "job progress", Action: "2 out of 5 complete", Current: 2, Total: 5, HideCounts: true}, {ID: "active tasks", Action: "2 out of 2 tasks"}, }) tasks[3].Status.State = swarm.TaskStateComplete tasks = append(tasks, swarm.Task{ ID: "task5", Slot: 4, NodeID: "", DesiredState: swarm.TaskStateComplete, Status: swarm.TaskStatus{State: swarm.TaskStateRunning}, JobIteration: &swarm.Version{Index: service.JobStatus.JobIteration.Index}, }, ) ut.testUpdater(tasks, false, []progress.Progress{ {ID: "1/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "2/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "3/5", Action: "running ", Current: 9, Total: 10, HideCounts: true}, {ID: "4/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "5/5", Action: "running ", Current: 9, Total: 10, HideCounts: true}, {ID: "job progress", Action: "3 out of 5 complete", Current: 3, Total: 5, HideCounts: true}, {ID: "active tasks", Action: "2 out of 2 tasks"}, }) tasks[2].Status.State = swarm.TaskStateFailed tasks[2].Status.Err = "the task failed" ut.testUpdater(tasks, false, []progress.Progress{ {ID: "1/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "2/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "3/5", Action: "the task failed"}, {ID: "4/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "5/5", Action: "running ", Current: 9, Total: 10, HideCounts: true}, {ID: "job progress", Action: "3 out of 5 complete", Current: 3, Total: 5, HideCounts: true}, {ID: "active tasks", Action: "1 out of 2 tasks"}, }) tasks[4].Status.State = swarm.TaskStateComplete tasks = append(tasks, swarm.Task{ ID: "task6", Slot: 2, NodeID: "", DesiredState: swarm.TaskStateComplete, Status: swarm.TaskStatus{State: swarm.TaskStateRunning}, JobIteration: &swarm.Version{Index: service.JobStatus.JobIteration.Index}, }, ) ut.testUpdater(tasks, false, []progress.Progress{ {ID: "1/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "2/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "3/5", Action: "running ", Current: 9, Total: 10, HideCounts: true}, {ID: "4/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "5/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "job progress", Action: "4 out of 5 complete", Current: 4, Total: 5, HideCounts: true}, {ID: "active tasks", Action: "1 out of 1 tasks"}, }) tasks[5].Status.State = swarm.TaskStateComplete ut.testUpdater(tasks, true, []progress.Progress{ {ID: "1/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "2/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "3/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "4/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "5/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "job progress", Action: "5 out of 5 complete", Current: 5, Total: 5, HideCounts: true}, {ID: "active tasks", Action: "0 out of 0 tasks"}, }) } func TestReplicatedJobProgressUpdaterLarge(t *testing.T) { concurrent := uint64(10) total := uint64(50) service := swarm.Service{ Spec: swarm.ServiceSpec{ Mode: swarm.ServiceMode{ ReplicatedJob: &swarm.ReplicatedJob{ MaxConcurrent: &concurrent, TotalCompletions: &total, }, }, }, JobStatus: &swarm.JobStatus{ JobIteration: swarm.Version{Index: 0}, }, } p := &mockProgress{} ut := updaterTester{ t: t, updater: newReplicatedJobProgressUpdater(service, p), p: p, activeNodes: map[string]struct{}{"a": {}, "b": {}}, service: service, } tasks := []swarm.Task{} // see the comments in TestReplicatedJobProgressUpdaterSmall for why // we write this out twice. ut.testUpdater(tasks, false, []progress.Progress{ {ID: "job progress", Action: " 0 out of 50 complete", Current: 0, Total: 50, HideCounts: true}, {ID: "active tasks", Action: " 0 out of 10 tasks"}, // we don't write out individual status bars for a large job, only the // overall progress bar {ID: "job progress", Action: " 0 out of 50 complete", Current: 0, Total: 50, HideCounts: true}, {ID: "active tasks", Action: " 0 out of 10 tasks"}, }) // first, create the initial batch of running tasks for i := 0; i < int(concurrent); i++ { tasks = append(tasks, swarm.Task{ ID: strconv.Itoa(i), Slot: i, DesiredState: swarm.TaskStateComplete, Status: swarm.TaskStatus{State: swarm.TaskStateNew}, JobIteration: &swarm.Version{Index: 0}, }) ut.testUpdater(tasks, false, []progress.Progress{ {ID: "job progress", Action: " 0 out of 50 complete", Current: 0, Total: 50, HideCounts: true}, {ID: "active tasks", Action: fmt.Sprintf("%2d out of 10 tasks", i+1)}, }) } // now, start moving tasks to completed, and starting new tasks after them. // to do this, we'll start at 0, mark a task complete, and then append a // new one. we'll stop before we get to the end, because the end has a // steadily decreasing denominator for the active tasks // // for 10 concurrent 50 total, this means we'll stop at 50 - 10 = 40 tasks // in the completed state, 10 tasks running. the last index in use will be // 39. for i := 0; i < int(total)-int(concurrent); i++ { tasks[i].Status.State = swarm.TaskStateComplete ut.testUpdater(tasks, false, []progress.Progress{ {ID: "job progress", Action: fmt.Sprintf("%2d out of 50 complete", i+1), Current: int64(i + 1), Total: 50, HideCounts: true}, {ID: "active tasks", Action: " 9 out of 10 tasks"}, }) last := len(tasks) tasks = append(tasks, swarm.Task{ ID: strconv.Itoa(last), Slot: last, DesiredState: swarm.TaskStateComplete, Status: swarm.TaskStatus{State: swarm.TaskStateNew}, JobIteration: &swarm.Version{Index: 0}, }) ut.testUpdater(tasks, false, []progress.Progress{ {ID: "job progress", Action: fmt.Sprintf("%2d out of 50 complete", i+1), Current: int64(i + 1), Total: 50, HideCounts: true}, {ID: "active tasks", Action: "10 out of 10 tasks"}, }) } // quick check, to make sure we did the math right when we wrote this code: // we do have 50 tasks in the slice, right? assert.Check(t, is.Equal(len(tasks), int(total))) // now, we're down to our last 10 tasks, which are all running. We need to // wind these down for i := int(total) - int(concurrent) - 1; i < int(total); i++ { tasks[i].Status.State = swarm.TaskStateComplete ut.testUpdater(tasks, (i+1 == int(total)), []progress.Progress{ {ID: "job progress", Action: fmt.Sprintf("%2d out of 50 complete", i+1), Current: int64(i + 1), Total: 50, HideCounts: true}, {ID: "active tasks", Action: fmt.Sprintf("%2[1]d out of %2[1]d tasks", int(total)-(i+1))}, }) } } func TestGlobalJobProgressUpdaterSmall(t *testing.T) { service := swarm.Service{ Spec: swarm.ServiceSpec{ Mode: swarm.ServiceMode{ GlobalJob: &swarm.GlobalJob{}, }, }, JobStatus: &swarm.JobStatus{ JobIteration: swarm.Version{Index: 1}, }, } p := &mockProgress{} ut := updaterTester{ t: t, updater: &globalJobProgressUpdater{ progressOut: p, }, p: p, activeNodes: map[string]struct{}{"a": {}, "b": {}, "c": {}}, service: service, } tasks := []swarm.Task{ { ID: "oldtask1", DesiredState: swarm.TaskStateComplete, Status: swarm.TaskStatus{State: swarm.TaskStateComplete}, JobIteration: &swarm.Version{Index: 0}, NodeID: "a", }, { ID: "oldtask2", DesiredState: swarm.TaskStateComplete, Status: swarm.TaskStatus{State: swarm.TaskStateComplete}, JobIteration: &swarm.Version{Index: 0}, NodeID: "b", }, { ID: "task1", DesiredState: swarm.TaskStateComplete, Status: swarm.TaskStatus{State: swarm.TaskStateNew}, JobIteration: &swarm.Version{Index: 1}, NodeID: "a", }, { ID: "task2", DesiredState: swarm.TaskStateComplete, Status: swarm.TaskStatus{State: swarm.TaskStateNew}, JobIteration: &swarm.Version{Index: 1}, NodeID: "b", }, { ID: "task3", DesiredState: swarm.TaskStateComplete, Status: swarm.TaskStatus{State: swarm.TaskStateNew}, JobIteration: &swarm.Version{Index: 1}, NodeID: "c", }, } // we don't know how many tasks will be created until we get the initial // task list, so we should not write out any definitive answers yet. ut.testUpdater([]swarm.Task{}, false, []progress.Progress{ {ID: "job progress", Action: "waiting for tasks"}, }) ut.testUpdaterNoOrder(tasks, false, []progress.Progress{ {ID: "job progress", Action: "0 out of 3 complete", Current: 0, Total: 3, HideCounts: true}, {ID: "a", Action: "new ", Current: 1, Total: 10, HideCounts: true}, {ID: "b", Action: "new ", Current: 1, Total: 10, HideCounts: true}, {ID: "c", Action: "new ", Current: 1, Total: 10, HideCounts: true}, {ID: "job progress", Action: "0 out of 3 complete", Current: 0, Total: 3, HideCounts: true}, }) tasks[2].Status.State = swarm.TaskStatePreparing tasks[3].Status.State = swarm.TaskStateRunning tasks[4].Status.State = swarm.TaskStateAccepted ut.testUpdaterNoOrder(tasks, false, []progress.Progress{ {ID: "a", Action: "preparing", Current: 6, Total: 10, HideCounts: true}, {ID: "b", Action: "running ", Current: 9, Total: 10, HideCounts: true}, {ID: "c", Action: "accepted ", Current: 5, Total: 10, HideCounts: true}, {ID: "job progress", Action: "0 out of 3 complete", Current: 0, Total: 3, HideCounts: true}, }) tasks[2].Status.State = swarm.TaskStateRunning tasks[3].Status.State = swarm.TaskStateComplete tasks[4].Status.State = swarm.TaskStateRunning ut.testUpdaterNoOrder(tasks, false, []progress.Progress{ {ID: "a", Action: "running ", Current: 9, Total: 10, HideCounts: true}, {ID: "b", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "c", Action: "running ", Current: 9, Total: 10, HideCounts: true}, {ID: "job progress", Action: "1 out of 3 complete", Current: 1, Total: 3, HideCounts: true}, }) tasks[2].Status.State = swarm.TaskStateFailed tasks[2].Status.Err = "task failed" tasks[4].Status.State = swarm.TaskStateComplete ut.testUpdaterNoOrder(tasks, false, []progress.Progress{ {ID: "a", Action: "task failed"}, {ID: "b", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "c", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "job progress", Action: "2 out of 3 complete", Current: 2, Total: 3, HideCounts: true}, }) tasks = append(tasks, swarm.Task{ ID: "task4", DesiredState: swarm.TaskStateComplete, Status: swarm.TaskStatus{State: swarm.TaskStatePreparing}, NodeID: tasks[2].NodeID, JobIteration: &swarm.Version{Index: 1}, }) ut.testUpdaterNoOrder(tasks, false, []progress.Progress{ {ID: "a", Action: "preparing", Current: 6, Total: 10, HideCounts: true}, {ID: "b", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "c", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "job progress", Action: "2 out of 3 complete", Current: 2, Total: 3, HideCounts: true}, }) tasks[5].Status.State = swarm.TaskStateComplete ut.testUpdaterNoOrder(tasks, true, []progress.Progress{ {ID: "a", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "b", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "c", Action: "complete ", Current: 10, Total: 10, HideCounts: true}, {ID: "job progress", Action: "3 out of 3 complete", Current: 3, Total: 3, HideCounts: true}, }) } func TestGlobalJobProgressUpdaterLarge(t *testing.T) { service := swarm.Service{ Spec: swarm.ServiceSpec{ Mode: swarm.ServiceMode{ GlobalJob: &swarm.GlobalJob{}, }, }, JobStatus: &swarm.JobStatus{ JobIteration: swarm.Version{Index: 1}, }, } activeNodes := map[string]struct{}{} for i := 0; i < 50; i++ { activeNodes[fmt.Sprintf("node%v", i)] = struct{}{} } p := &mockProgress{} ut := updaterTester{ t: t, updater: &globalJobProgressUpdater{ progressOut: p, }, p: p, activeNodes: activeNodes, service: service, } tasks := []swarm.Task{} for nodeID := range activeNodes { tasks = append(tasks, swarm.Task{ ID: fmt.Sprintf("task%s", nodeID), NodeID: nodeID, DesiredState: swarm.TaskStateComplete, Status: swarm.TaskStatus{ State: swarm.TaskStateNew, }, JobIteration: &swarm.Version{Index: 1}, }) } // no bars, because too many tasks ut.testUpdater(tasks, false, []progress.Progress{ {ID: "job progress", Action: " 0 out of 50 complete", Current: 0, Total: 50, HideCounts: true}, {ID: "job progress", Action: " 0 out of 50 complete", Current: 0, Total: 50, HideCounts: true}, }) for i := range tasks { tasks[i].Status.State = swarm.TaskStateComplete ut.testUpdater(tasks, i+1 == len(activeNodes), []progress.Progress{ { ID: "job progress", Action: fmt.Sprintf("%2d out of 50 complete", i+1), Current: int64(i + 1), Total: 50, HideCounts: true, }, }) } }