github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/cli/command/service/progress/progress_test.go (about)

     1  package progress
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"testing"
     7  
     8  	"github.com/docker/docker/api/types/swarm"
     9  	"github.com/docker/docker/pkg/progress"
    10  	"gotest.tools/v3/assert"
    11  	is "gotest.tools/v3/assert/cmp"
    12  )
    13  
    14  type mockProgress struct {
    15  	p []progress.Progress
    16  }
    17  
    18  func (mp *mockProgress) WriteProgress(p progress.Progress) error {
    19  	mp.p = append(mp.p, p)
    20  	return nil
    21  }
    22  
    23  func (mp *mockProgress) clear() {
    24  	mp.p = nil
    25  }
    26  
    27  type updaterTester struct {
    28  	t           *testing.T
    29  	updater     progressUpdater
    30  	p           *mockProgress
    31  	service     swarm.Service
    32  	activeNodes map[string]struct{}
    33  	rollback    bool
    34  }
    35  
    36  func (u updaterTester) testUpdater(tasks []swarm.Task, expectedConvergence bool, expectedProgress []progress.Progress) {
    37  	u.p.clear()
    38  
    39  	converged, err := u.updater.update(u.service, tasks, u.activeNodes, u.rollback)
    40  	assert.Check(u.t, err)
    41  	assert.Check(u.t, is.Equal(expectedConvergence, converged))
    42  	assert.Check(u.t, is.DeepEqual(expectedProgress, u.p.p))
    43  }
    44  
    45  func (u updaterTester) testUpdaterNoOrder(tasks []swarm.Task, expectedConvergence bool, expectedProgress []progress.Progress) {
    46  	u.p.clear()
    47  	converged, err := u.updater.update(u.service, tasks, u.activeNodes, u.rollback)
    48  	assert.Check(u.t, err)
    49  	assert.Check(u.t, is.Equal(expectedConvergence, converged))
    50  
    51  	// instead of checking that expected and actual match exactly, verify that
    52  	// they are the same length, and every time from actual is in expected.
    53  	assert.Check(u.t, is.Equal(len(expectedProgress), len(u.p.p)))
    54  	for _, prog := range expectedProgress {
    55  		assert.Check(u.t, is.Contains(u.p.p, prog))
    56  	}
    57  }
    58  
    59  func TestReplicatedProgressUpdaterOneReplica(t *testing.T) {
    60  	replicas := uint64(1)
    61  
    62  	service := swarm.Service{
    63  		Spec: swarm.ServiceSpec{
    64  			Mode: swarm.ServiceMode{
    65  				Replicated: &swarm.ReplicatedService{
    66  					Replicas: &replicas,
    67  				},
    68  			},
    69  		},
    70  	}
    71  
    72  	p := &mockProgress{}
    73  	updaterTester := updaterTester{
    74  		t: t,
    75  		updater: &replicatedProgressUpdater{
    76  			progressOut: p,
    77  		},
    78  		p:           p,
    79  		activeNodes: map[string]struct{}{"a": {}, "b": {}},
    80  		service:     service,
    81  	}
    82  
    83  	tasks := []swarm.Task{}
    84  
    85  	updaterTester.testUpdater(tasks, false,
    86  		[]progress.Progress{
    87  			{ID: "overall progress", Action: "0 out of 1 tasks"},
    88  			{ID: "1/1", Action: " "},
    89  			{ID: "overall progress", Action: "0 out of 1 tasks"},
    90  		})
    91  
    92  	// Task with DesiredState beyond Running is ignored
    93  	tasks = append(tasks,
    94  		swarm.Task{
    95  			ID:           "1",
    96  			NodeID:       "a",
    97  			DesiredState: swarm.TaskStateShutdown,
    98  			Status:       swarm.TaskStatus{State: swarm.TaskStateNew},
    99  		})
   100  	updaterTester.testUpdater(tasks, false,
   101  		[]progress.Progress{
   102  			{ID: "overall progress", Action: "0 out of 1 tasks"},
   103  		})
   104  
   105  	// Task with valid DesiredState and State updates progress bar
   106  	tasks[0].DesiredState = swarm.TaskStateRunning
   107  	updaterTester.testUpdater(tasks, false,
   108  		[]progress.Progress{
   109  			{ID: "1/1", Action: "new      ", Current: 1, Total: 9, HideCounts: true},
   110  			{ID: "overall progress", Action: "0 out of 1 tasks"},
   111  		})
   112  
   113  	// If the task exposes an error, we should show that instead of the
   114  	// progress bar.
   115  	tasks[0].Status.Err = "something is wrong"
   116  	updaterTester.testUpdater(tasks, false,
   117  		[]progress.Progress{
   118  			{ID: "1/1", Action: "something is wrong"},
   119  			{ID: "overall progress", Action: "0 out of 1 tasks"},
   120  		})
   121  
   122  	// When the task reaches running, update should return true
   123  	tasks[0].Status.Err = ""
   124  	tasks[0].Status.State = swarm.TaskStateRunning
   125  	updaterTester.testUpdater(tasks, true,
   126  		[]progress.Progress{
   127  			{ID: "1/1", Action: "running  ", Current: 9, Total: 9, HideCounts: true},
   128  			{ID: "overall progress", Action: "1 out of 1 tasks"},
   129  		})
   130  
   131  	// If the task fails, update should return false again
   132  	tasks[0].Status.Err = "task failed"
   133  	tasks[0].Status.State = swarm.TaskStateFailed
   134  	updaterTester.testUpdater(tasks, false,
   135  		[]progress.Progress{
   136  			{ID: "1/1", Action: "task failed"},
   137  			{ID: "overall progress", Action: "0 out of 1 tasks"},
   138  		})
   139  
   140  	// If the task is restarted, progress output should be shown for the
   141  	// replacement task, not the old task.
   142  	tasks[0].DesiredState = swarm.TaskStateShutdown
   143  	tasks = append(tasks,
   144  		swarm.Task{
   145  			ID:           "2",
   146  			NodeID:       "b",
   147  			DesiredState: swarm.TaskStateRunning,
   148  			Status:       swarm.TaskStatus{State: swarm.TaskStateRunning},
   149  		})
   150  	updaterTester.testUpdater(tasks, true,
   151  		[]progress.Progress{
   152  			{ID: "1/1", Action: "running  ", Current: 9, Total: 9, HideCounts: true},
   153  			{ID: "overall progress", Action: "1 out of 1 tasks"},
   154  		})
   155  
   156  	// Add a new task while the current one is still running, to simulate
   157  	// "start-then-stop" updates.
   158  	tasks = append(tasks,
   159  		swarm.Task{
   160  			ID:           "3",
   161  			NodeID:       "b",
   162  			DesiredState: swarm.TaskStateRunning,
   163  			Status:       swarm.TaskStatus{State: swarm.TaskStatePreparing},
   164  		})
   165  	updaterTester.testUpdater(tasks, false,
   166  		[]progress.Progress{
   167  			{ID: "1/1", Action: "preparing", Current: 6, Total: 9, HideCounts: true},
   168  			{ID: "overall progress", Action: "0 out of 1 tasks"},
   169  		})
   170  }
   171  
   172  func TestReplicatedProgressUpdaterManyReplicas(t *testing.T) {
   173  	replicas := uint64(50)
   174  
   175  	service := swarm.Service{
   176  		Spec: swarm.ServiceSpec{
   177  			Mode: swarm.ServiceMode{
   178  				Replicated: &swarm.ReplicatedService{
   179  					Replicas: &replicas,
   180  				},
   181  			},
   182  		},
   183  	}
   184  
   185  	p := &mockProgress{}
   186  	updaterTester := updaterTester{
   187  		t: t,
   188  		updater: &replicatedProgressUpdater{
   189  			progressOut: p,
   190  		},
   191  		p:           p,
   192  		activeNodes: map[string]struct{}{"a": {}, "b": {}},
   193  		service:     service,
   194  	}
   195  
   196  	tasks := []swarm.Task{}
   197  
   198  	// No per-task progress bars because there are too many replicas
   199  	updaterTester.testUpdater(tasks, false,
   200  		[]progress.Progress{
   201  			{ID: "overall progress", Action: fmt.Sprintf("0 out of %d tasks", replicas)},
   202  			{ID: "overall progress", Action: fmt.Sprintf("0 out of %d tasks", replicas)},
   203  		})
   204  
   205  	for i := 0; i != int(replicas); i++ {
   206  		tasks = append(tasks,
   207  			swarm.Task{
   208  				ID:           strconv.Itoa(i),
   209  				Slot:         i + 1,
   210  				NodeID:       "a",
   211  				DesiredState: swarm.TaskStateRunning,
   212  				Status:       swarm.TaskStatus{State: swarm.TaskStateNew},
   213  			})
   214  
   215  		if i%2 == 1 {
   216  			tasks[i].NodeID = "b"
   217  		}
   218  		updaterTester.testUpdater(tasks, false,
   219  			[]progress.Progress{
   220  				{ID: "overall progress", Action: fmt.Sprintf("%d out of %d tasks", i, replicas)},
   221  			})
   222  
   223  		tasks[i].Status.State = swarm.TaskStateRunning
   224  		updaterTester.testUpdater(tasks, uint64(i) == replicas-1,
   225  			[]progress.Progress{
   226  				{ID: "overall progress", Action: fmt.Sprintf("%d out of %d tasks", i+1, replicas)},
   227  			})
   228  	}
   229  }
   230  
   231  func TestGlobalProgressUpdaterOneNode(t *testing.T) {
   232  	service := swarm.Service{
   233  		Spec: swarm.ServiceSpec{
   234  			Mode: swarm.ServiceMode{
   235  				Global: &swarm.GlobalService{},
   236  			},
   237  		},
   238  	}
   239  
   240  	p := &mockProgress{}
   241  	updaterTester := updaterTester{
   242  		t: t,
   243  		updater: &globalProgressUpdater{
   244  			progressOut: p,
   245  		},
   246  		p:           p,
   247  		activeNodes: map[string]struct{}{"a": {}, "b": {}},
   248  		service:     service,
   249  	}
   250  
   251  	tasks := []swarm.Task{}
   252  
   253  	updaterTester.testUpdater(tasks, false,
   254  		[]progress.Progress{
   255  			{ID: "overall progress", Action: "waiting for new tasks"},
   256  		})
   257  
   258  	// Task with DesiredState beyond Running is ignored
   259  	tasks = append(tasks,
   260  		swarm.Task{
   261  			ID:           "1",
   262  			NodeID:       "a",
   263  			DesiredState: swarm.TaskStateShutdown,
   264  			Status:       swarm.TaskStatus{State: swarm.TaskStateNew},
   265  		})
   266  	updaterTester.testUpdater(tasks, false,
   267  		[]progress.Progress{
   268  			{ID: "overall progress", Action: "0 out of 1 tasks"},
   269  			{ID: "overall progress", Action: "0 out of 1 tasks"},
   270  		})
   271  
   272  	// Task with valid DesiredState and State updates progress bar
   273  	tasks[0].DesiredState = swarm.TaskStateRunning
   274  	updaterTester.testUpdater(tasks, false,
   275  		[]progress.Progress{
   276  			{ID: "a", Action: "new      ", Current: 1, Total: 9, HideCounts: true},
   277  			{ID: "overall progress", Action: "0 out of 1 tasks"},
   278  		})
   279  
   280  	// If the task exposes an error, we should show that instead of the
   281  	// progress bar.
   282  	tasks[0].Status.Err = "something is wrong"
   283  	updaterTester.testUpdater(tasks, false,
   284  		[]progress.Progress{
   285  			{ID: "a", Action: "something is wrong"},
   286  			{ID: "overall progress", Action: "0 out of 1 tasks"},
   287  		})
   288  
   289  	// When the task reaches running, update should return true
   290  	tasks[0].Status.Err = ""
   291  	tasks[0].Status.State = swarm.TaskStateRunning
   292  	updaterTester.testUpdater(tasks, true,
   293  		[]progress.Progress{
   294  			{ID: "a", Action: "running  ", Current: 9, Total: 9, HideCounts: true},
   295  			{ID: "overall progress", Action: "1 out of 1 tasks"},
   296  		})
   297  
   298  	// If the task fails, update should return false again
   299  	tasks[0].Status.Err = "task failed"
   300  	tasks[0].Status.State = swarm.TaskStateFailed
   301  	updaterTester.testUpdater(tasks, false,
   302  		[]progress.Progress{
   303  			{ID: "a", Action: "task failed"},
   304  			{ID: "overall progress", Action: "0 out of 1 tasks"},
   305  		})
   306  
   307  	// If the task is restarted, progress output should be shown for the
   308  	// replacement task, not the old task.
   309  	tasks[0].DesiredState = swarm.TaskStateShutdown
   310  	tasks = append(tasks,
   311  		swarm.Task{
   312  			ID:           "2",
   313  			NodeID:       "a",
   314  			DesiredState: swarm.TaskStateRunning,
   315  			Status:       swarm.TaskStatus{State: swarm.TaskStateRunning},
   316  		})
   317  	updaterTester.testUpdater(tasks, true,
   318  		[]progress.Progress{
   319  			{ID: "a", Action: "running  ", Current: 9, Total: 9, HideCounts: true},
   320  			{ID: "overall progress", Action: "1 out of 1 tasks"},
   321  		})
   322  
   323  	// Add a new task while the current one is still running, to simulate
   324  	// "start-then-stop" updates.
   325  	tasks = append(tasks,
   326  		swarm.Task{
   327  			ID:           "3",
   328  			NodeID:       "a",
   329  			DesiredState: swarm.TaskStateRunning,
   330  			Status:       swarm.TaskStatus{State: swarm.TaskStatePreparing},
   331  		})
   332  	updaterTester.testUpdater(tasks, false,
   333  		[]progress.Progress{
   334  			{ID: "a", Action: "preparing", Current: 6, Total: 9, HideCounts: true},
   335  			{ID: "overall progress", Action: "0 out of 1 tasks"},
   336  		})
   337  }
   338  
   339  func TestGlobalProgressUpdaterManyNodes(t *testing.T) {
   340  	nodes := 50
   341  
   342  	service := swarm.Service{
   343  		Spec: swarm.ServiceSpec{
   344  			Mode: swarm.ServiceMode{
   345  				Global: &swarm.GlobalService{},
   346  			},
   347  		},
   348  	}
   349  
   350  	p := &mockProgress{}
   351  	updaterTester := updaterTester{
   352  		t: t,
   353  		updater: &globalProgressUpdater{
   354  			progressOut: p,
   355  		},
   356  		p:           p,
   357  		activeNodes: map[string]struct{}{},
   358  		service:     service,
   359  	}
   360  
   361  	for i := 0; i != nodes; i++ {
   362  		updaterTester.activeNodes[strconv.Itoa(i)] = struct{}{}
   363  	}
   364  
   365  	tasks := []swarm.Task{}
   366  
   367  	updaterTester.testUpdater(tasks, false,
   368  		[]progress.Progress{
   369  			{ID: "overall progress", Action: "waiting for new tasks"},
   370  		})
   371  
   372  	for i := 0; i != nodes; i++ {
   373  		tasks = append(tasks,
   374  			swarm.Task{
   375  				ID:           "task" + strconv.Itoa(i),
   376  				NodeID:       strconv.Itoa(i),
   377  				DesiredState: swarm.TaskStateRunning,
   378  				Status:       swarm.TaskStatus{State: swarm.TaskStateNew},
   379  			})
   380  	}
   381  
   382  	updaterTester.testUpdater(tasks, false,
   383  		[]progress.Progress{
   384  			{ID: "overall progress", Action: fmt.Sprintf("0 out of %d tasks", nodes)},
   385  			{ID: "overall progress", Action: fmt.Sprintf("0 out of %d tasks", nodes)},
   386  		})
   387  
   388  	for i := 0; i != nodes; i++ {
   389  		tasks[i].Status.State = swarm.TaskStateRunning
   390  		updaterTester.testUpdater(tasks, i == nodes-1,
   391  			[]progress.Progress{
   392  				{ID: "overall progress", Action: fmt.Sprintf("%d out of %d tasks", i+1, nodes)},
   393  			})
   394  	}
   395  }
   396  
   397  func TestReplicatedJobProgressUpdaterSmall(t *testing.T) {
   398  	concurrent := uint64(2)
   399  	total := uint64(5)
   400  
   401  	service := swarm.Service{
   402  		Spec: swarm.ServiceSpec{
   403  			Mode: swarm.ServiceMode{
   404  				ReplicatedJob: &swarm.ReplicatedJob{
   405  					MaxConcurrent:    &concurrent,
   406  					TotalCompletions: &total,
   407  				},
   408  			},
   409  		},
   410  		JobStatus: &swarm.JobStatus{
   411  			JobIteration: swarm.Version{Index: 1},
   412  		},
   413  	}
   414  
   415  	p := &mockProgress{}
   416  	ut := updaterTester{
   417  		t:           t,
   418  		updater:     newReplicatedJobProgressUpdater(service, p),
   419  		p:           p,
   420  		activeNodes: map[string]struct{}{"a": {}, "b": {}},
   421  		service:     service,
   422  	}
   423  
   424  	// create some tasks belonging to a previous iteration
   425  	tasks := []swarm.Task{
   426  		{
   427  			ID:           "oldtask1",
   428  			Slot:         0,
   429  			NodeID:       "",
   430  			DesiredState: swarm.TaskStateComplete,
   431  			Status:       swarm.TaskStatus{State: swarm.TaskStateNew},
   432  			JobIteration: &swarm.Version{Index: 0},
   433  		}, {
   434  			ID:           "oldtask2",
   435  			Slot:         1,
   436  			NodeID:       "",
   437  			DesiredState: swarm.TaskStateComplete,
   438  			Status:       swarm.TaskStatus{State: swarm.TaskStateComplete},
   439  			JobIteration: &swarm.Version{Index: 0},
   440  		},
   441  	}
   442  
   443  	ut.testUpdater(tasks, false, []progress.Progress{
   444  		// on the initial pass, we draw all of the progress bars at once, which
   445  		// puts them in order for the rest of the operation
   446  		{ID: "job progress", Action: "0 out of 5 complete", Current: 0, Total: 5, HideCounts: true},
   447  		{ID: "active tasks", Action: "0 out of 2 tasks"},
   448  		{ID: "1/5", Action: " "},
   449  		{ID: "2/5", Action: " "},
   450  		{ID: "3/5", Action: " "},
   451  		{ID: "4/5", Action: " "},
   452  		{ID: "5/5", Action: " "},
   453  		// from here on, we draw as normal. as a side effect, we will have a
   454  		// second update for the job progress and active tasks. This has no
   455  		// practical effect on the UI, it's just a side effect of the update
   456  		// logic.
   457  		{ID: "job progress", Action: "0 out of 5 complete", Current: 0, Total: 5, HideCounts: true},
   458  		{ID: "active tasks", Action: "0 out of 2 tasks"},
   459  	})
   460  
   461  	// wipe the old tasks out of the list
   462  	tasks = []swarm.Task{}
   463  	tasks = append(tasks,
   464  		swarm.Task{
   465  			ID:           "task1",
   466  			Slot:         0,
   467  			NodeID:       "",
   468  			DesiredState: swarm.TaskStateComplete,
   469  			Status:       swarm.TaskStatus{State: swarm.TaskStateNew},
   470  			JobIteration: &swarm.Version{Index: service.JobStatus.JobIteration.Index},
   471  		},
   472  		swarm.Task{
   473  			ID:           "task2",
   474  			Slot:         1,
   475  			NodeID:       "",
   476  			DesiredState: swarm.TaskStateComplete,
   477  			Status:       swarm.TaskStatus{State: swarm.TaskStateNew},
   478  			JobIteration: &swarm.Version{Index: service.JobStatus.JobIteration.Index},
   479  		},
   480  	)
   481  	ut.testUpdater(tasks, false, []progress.Progress{
   482  		{ID: "1/5", Action: "new      ", Current: 1, Total: 10, HideCounts: true},
   483  		{ID: "2/5", Action: "new      ", Current: 1, Total: 10, HideCounts: true},
   484  		{ID: "job progress", Action: "0 out of 5 complete", Current: 0, Total: 5, HideCounts: true},
   485  		{ID: "active tasks", Action: "2 out of 2 tasks"},
   486  	})
   487  
   488  	tasks[0].Status.State = swarm.TaskStatePreparing
   489  	tasks[1].Status.State = swarm.TaskStateAssigned
   490  	ut.testUpdater(tasks, false, []progress.Progress{
   491  		{ID: "1/5", Action: "preparing", Current: 6, Total: 10, HideCounts: true},
   492  		{ID: "2/5", Action: "assigned ", Current: 4, Total: 10, HideCounts: true},
   493  		{ID: "job progress", Action: "0 out of 5 complete", Current: 0, Total: 5, HideCounts: true},
   494  		{ID: "active tasks", Action: "2 out of 2 tasks"},
   495  	})
   496  
   497  	tasks[0].Status.State = swarm.TaskStateRunning
   498  	tasks[1].Status.State = swarm.TaskStatePreparing
   499  	ut.testUpdater(tasks, false, []progress.Progress{
   500  		{ID: "1/5", Action: "running  ", Current: 9, Total: 10, HideCounts: true},
   501  		{ID: "2/5", Action: "preparing", Current: 6, Total: 10, HideCounts: true},
   502  		{ID: "job progress", Action: "0 out of 5 complete", Current: 0, Total: 5, HideCounts: true},
   503  		{ID: "active tasks", Action: "2 out of 2 tasks"},
   504  	})
   505  
   506  	tasks[0].Status.State = swarm.TaskStateComplete
   507  	tasks[1].Status.State = swarm.TaskStateComplete
   508  	ut.testUpdater(tasks, false, []progress.Progress{
   509  		{ID: "1/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   510  		{ID: "2/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   511  		{ID: "job progress", Action: "2 out of 5 complete", Current: 2, Total: 5, HideCounts: true},
   512  		{ID: "active tasks", Action: "0 out of 2 tasks"},
   513  	})
   514  
   515  	tasks = append(tasks,
   516  		swarm.Task{
   517  			ID:           "task3",
   518  			Slot:         2,
   519  			NodeID:       "",
   520  			DesiredState: swarm.TaskStateComplete,
   521  			Status:       swarm.TaskStatus{State: swarm.TaskStateNew},
   522  			JobIteration: &swarm.Version{Index: service.JobStatus.JobIteration.Index},
   523  		},
   524  		swarm.Task{
   525  			ID:           "task4",
   526  			Slot:         3,
   527  			NodeID:       "",
   528  			DesiredState: swarm.TaskStateComplete,
   529  			Status:       swarm.TaskStatus{State: swarm.TaskStateNew},
   530  			JobIteration: &swarm.Version{Index: service.JobStatus.JobIteration.Index},
   531  		},
   532  	)
   533  
   534  	ut.testUpdater(tasks, false, []progress.Progress{
   535  		{ID: "1/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   536  		{ID: "2/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   537  		{ID: "3/5", Action: "new      ", Current: 1, Total: 10, HideCounts: true},
   538  		{ID: "4/5", Action: "new      ", Current: 1, Total: 10, HideCounts: true},
   539  		{ID: "job progress", Action: "2 out of 5 complete", Current: 2, Total: 5, HideCounts: true},
   540  		{ID: "active tasks", Action: "2 out of 2 tasks"},
   541  	})
   542  
   543  	tasks[2].Status.State = swarm.TaskStateRunning
   544  	tasks[3].Status.State = swarm.TaskStateRunning
   545  	ut.testUpdater(tasks, false, []progress.Progress{
   546  		{ID: "1/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   547  		{ID: "2/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   548  		{ID: "3/5", Action: "running  ", Current: 9, Total: 10, HideCounts: true},
   549  		{ID: "4/5", Action: "running  ", Current: 9, Total: 10, HideCounts: true},
   550  		{ID: "job progress", Action: "2 out of 5 complete", Current: 2, Total: 5, HideCounts: true},
   551  		{ID: "active tasks", Action: "2 out of 2 tasks"},
   552  	})
   553  
   554  	tasks[3].Status.State = swarm.TaskStateComplete
   555  	tasks = append(tasks,
   556  		swarm.Task{
   557  			ID:           "task5",
   558  			Slot:         4,
   559  			NodeID:       "",
   560  			DesiredState: swarm.TaskStateComplete,
   561  			Status:       swarm.TaskStatus{State: swarm.TaskStateRunning},
   562  			JobIteration: &swarm.Version{Index: service.JobStatus.JobIteration.Index},
   563  		},
   564  	)
   565  	ut.testUpdater(tasks, false, []progress.Progress{
   566  		{ID: "1/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   567  		{ID: "2/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   568  		{ID: "3/5", Action: "running  ", Current: 9, Total: 10, HideCounts: true},
   569  		{ID: "4/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   570  		{ID: "5/5", Action: "running  ", Current: 9, Total: 10, HideCounts: true},
   571  		{ID: "job progress", Action: "3 out of 5 complete", Current: 3, Total: 5, HideCounts: true},
   572  		{ID: "active tasks", Action: "2 out of 2 tasks"},
   573  	})
   574  
   575  	tasks[2].Status.State = swarm.TaskStateFailed
   576  	tasks[2].Status.Err = "the task failed"
   577  	ut.testUpdater(tasks, false, []progress.Progress{
   578  		{ID: "1/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   579  		{ID: "2/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   580  		{ID: "3/5", Action: "the task failed"},
   581  		{ID: "4/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   582  		{ID: "5/5", Action: "running  ", Current: 9, Total: 10, HideCounts: true},
   583  		{ID: "job progress", Action: "3 out of 5 complete", Current: 3, Total: 5, HideCounts: true},
   584  		{ID: "active tasks", Action: "1 out of 2 tasks"},
   585  	})
   586  
   587  	tasks[4].Status.State = swarm.TaskStateComplete
   588  	tasks = append(tasks,
   589  		swarm.Task{
   590  			ID:           "task6",
   591  			Slot:         2,
   592  			NodeID:       "",
   593  			DesiredState: swarm.TaskStateComplete,
   594  			Status:       swarm.TaskStatus{State: swarm.TaskStateRunning},
   595  			JobIteration: &swarm.Version{Index: service.JobStatus.JobIteration.Index},
   596  		},
   597  	)
   598  	ut.testUpdater(tasks, false, []progress.Progress{
   599  		{ID: "1/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   600  		{ID: "2/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   601  		{ID: "3/5", Action: "running  ", Current: 9, Total: 10, HideCounts: true},
   602  		{ID: "4/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   603  		{ID: "5/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   604  		{ID: "job progress", Action: "4 out of 5 complete", Current: 4, Total: 5, HideCounts: true},
   605  		{ID: "active tasks", Action: "1 out of 1 tasks"},
   606  	})
   607  
   608  	tasks[5].Status.State = swarm.TaskStateComplete
   609  	ut.testUpdater(tasks, true, []progress.Progress{
   610  		{ID: "1/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   611  		{ID: "2/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   612  		{ID: "3/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   613  		{ID: "4/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   614  		{ID: "5/5", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   615  		{ID: "job progress", Action: "5 out of 5 complete", Current: 5, Total: 5, HideCounts: true},
   616  		{ID: "active tasks", Action: "0 out of 0 tasks"},
   617  	})
   618  }
   619  
   620  func TestReplicatedJobProgressUpdaterLarge(t *testing.T) {
   621  	concurrent := uint64(10)
   622  	total := uint64(50)
   623  
   624  	service := swarm.Service{
   625  		Spec: swarm.ServiceSpec{
   626  			Mode: swarm.ServiceMode{
   627  				ReplicatedJob: &swarm.ReplicatedJob{
   628  					MaxConcurrent:    &concurrent,
   629  					TotalCompletions: &total,
   630  				},
   631  			},
   632  		},
   633  		JobStatus: &swarm.JobStatus{
   634  			JobIteration: swarm.Version{Index: 0},
   635  		},
   636  	}
   637  
   638  	p := &mockProgress{}
   639  	ut := updaterTester{
   640  		t:           t,
   641  		updater:     newReplicatedJobProgressUpdater(service, p),
   642  		p:           p,
   643  		activeNodes: map[string]struct{}{"a": {}, "b": {}},
   644  		service:     service,
   645  	}
   646  
   647  	tasks := []swarm.Task{}
   648  
   649  	// see the comments in TestReplicatedJobProgressUpdaterSmall for why
   650  	// we write this out twice.
   651  	ut.testUpdater(tasks, false, []progress.Progress{
   652  		{ID: "job progress", Action: " 0 out of 50 complete", Current: 0, Total: 50, HideCounts: true},
   653  		{ID: "active tasks", Action: " 0 out of 10 tasks"},
   654  		// we don't write out individual status bars for a large job, only the
   655  		// overall progress bar
   656  		{ID: "job progress", Action: " 0 out of 50 complete", Current: 0, Total: 50, HideCounts: true},
   657  		{ID: "active tasks", Action: " 0 out of 10 tasks"},
   658  	})
   659  
   660  	// first, create the initial batch of running tasks
   661  	for i := 0; i < int(concurrent); i++ {
   662  		tasks = append(tasks, swarm.Task{
   663  			ID:           strconv.Itoa(i),
   664  			Slot:         i,
   665  			DesiredState: swarm.TaskStateComplete,
   666  			Status:       swarm.TaskStatus{State: swarm.TaskStateNew},
   667  			JobIteration: &swarm.Version{Index: 0},
   668  		})
   669  
   670  		ut.testUpdater(tasks, false, []progress.Progress{
   671  			{ID: "job progress", Action: " 0 out of 50 complete", Current: 0, Total: 50, HideCounts: true},
   672  			{ID: "active tasks", Action: fmt.Sprintf("%2d out of 10 tasks", i+1)},
   673  		})
   674  	}
   675  
   676  	// now, start moving tasks to completed, and starting new tasks after them.
   677  	// to do this, we'll start at 0, mark a task complete, and then append a
   678  	// new one. we'll stop before we get to the end, because the end has a
   679  	// steadily decreasing denominator for the active tasks
   680  	//
   681  	// for 10 concurrent 50 total, this means we'll stop at 50 - 10 = 40 tasks
   682  	// in the completed state, 10 tasks running. the last index in use will be
   683  	// 39.
   684  	for i := 0; i < int(total)-int(concurrent); i++ {
   685  		tasks[i].Status.State = swarm.TaskStateComplete
   686  		ut.testUpdater(tasks, false, []progress.Progress{
   687  			{ID: "job progress", Action: fmt.Sprintf("%2d out of 50 complete", i+1), Current: int64(i + 1), Total: 50, HideCounts: true},
   688  			{ID: "active tasks", Action: " 9 out of 10 tasks"},
   689  		})
   690  
   691  		last := len(tasks)
   692  		tasks = append(tasks, swarm.Task{
   693  			ID:           strconv.Itoa(last),
   694  			Slot:         last,
   695  			DesiredState: swarm.TaskStateComplete,
   696  			Status:       swarm.TaskStatus{State: swarm.TaskStateNew},
   697  			JobIteration: &swarm.Version{Index: 0},
   698  		})
   699  
   700  		ut.testUpdater(tasks, false, []progress.Progress{
   701  			{ID: "job progress", Action: fmt.Sprintf("%2d out of 50 complete", i+1), Current: int64(i + 1), Total: 50, HideCounts: true},
   702  			{ID: "active tasks", Action: "10 out of 10 tasks"},
   703  		})
   704  	}
   705  
   706  	// quick check, to make sure we did the math right when we wrote this code:
   707  	// we do have 50 tasks in the slice, right?
   708  	assert.Check(t, is.Equal(len(tasks), int(total)))
   709  
   710  	// now, we're down to our last 10 tasks, which are all running. We need to
   711  	// wind these down
   712  	for i := int(total) - int(concurrent) - 1; i < int(total); i++ {
   713  		tasks[i].Status.State = swarm.TaskStateComplete
   714  		ut.testUpdater(tasks, (i+1 == int(total)), []progress.Progress{
   715  			{ID: "job progress", Action: fmt.Sprintf("%2d out of 50 complete", i+1), Current: int64(i + 1), Total: 50, HideCounts: true},
   716  			{ID: "active tasks", Action: fmt.Sprintf("%2[1]d out of %2[1]d tasks", int(total)-(i+1))},
   717  		})
   718  	}
   719  }
   720  
   721  func TestGlobalJobProgressUpdaterSmall(t *testing.T) {
   722  	service := swarm.Service{
   723  		Spec: swarm.ServiceSpec{
   724  			Mode: swarm.ServiceMode{
   725  				GlobalJob: &swarm.GlobalJob{},
   726  			},
   727  		},
   728  		JobStatus: &swarm.JobStatus{
   729  			JobIteration: swarm.Version{Index: 1},
   730  		},
   731  	}
   732  
   733  	p := &mockProgress{}
   734  	ut := updaterTester{
   735  		t: t,
   736  		updater: &globalJobProgressUpdater{
   737  			progressOut: p,
   738  		},
   739  		p:           p,
   740  		activeNodes: map[string]struct{}{"a": {}, "b": {}, "c": {}},
   741  		service:     service,
   742  	}
   743  
   744  	tasks := []swarm.Task{
   745  		{
   746  			ID:           "oldtask1",
   747  			DesiredState: swarm.TaskStateComplete,
   748  			Status:       swarm.TaskStatus{State: swarm.TaskStateComplete},
   749  			JobIteration: &swarm.Version{Index: 0},
   750  			NodeID:       "a",
   751  		}, {
   752  			ID:           "oldtask2",
   753  			DesiredState: swarm.TaskStateComplete,
   754  			Status:       swarm.TaskStatus{State: swarm.TaskStateComplete},
   755  			JobIteration: &swarm.Version{Index: 0},
   756  			NodeID:       "b",
   757  		}, {
   758  			ID:           "task1",
   759  			DesiredState: swarm.TaskStateComplete,
   760  			Status:       swarm.TaskStatus{State: swarm.TaskStateNew},
   761  			JobIteration: &swarm.Version{Index: 1},
   762  			NodeID:       "a",
   763  		}, {
   764  			ID:           "task2",
   765  			DesiredState: swarm.TaskStateComplete,
   766  			Status:       swarm.TaskStatus{State: swarm.TaskStateNew},
   767  			JobIteration: &swarm.Version{Index: 1},
   768  			NodeID:       "b",
   769  		}, {
   770  			ID:           "task3",
   771  			DesiredState: swarm.TaskStateComplete,
   772  			Status:       swarm.TaskStatus{State: swarm.TaskStateNew},
   773  			JobIteration: &swarm.Version{Index: 1},
   774  			NodeID:       "c",
   775  		},
   776  	}
   777  
   778  	// we don't know how many tasks will be created until we get the initial
   779  	// task list, so we should not write out any definitive answers yet.
   780  	ut.testUpdater([]swarm.Task{}, false, []progress.Progress{
   781  		{ID: "job progress", Action: "waiting for tasks"},
   782  	})
   783  
   784  	ut.testUpdaterNoOrder(tasks, false, []progress.Progress{
   785  		{ID: "job progress", Action: "0 out of 3 complete", Current: 0, Total: 3, HideCounts: true},
   786  		{ID: "a", Action: "new      ", Current: 1, Total: 10, HideCounts: true},
   787  		{ID: "b", Action: "new      ", Current: 1, Total: 10, HideCounts: true},
   788  		{ID: "c", Action: "new      ", Current: 1, Total: 10, HideCounts: true},
   789  		{ID: "job progress", Action: "0 out of 3 complete", Current: 0, Total: 3, HideCounts: true},
   790  	})
   791  
   792  	tasks[2].Status.State = swarm.TaskStatePreparing
   793  	tasks[3].Status.State = swarm.TaskStateRunning
   794  	tasks[4].Status.State = swarm.TaskStateAccepted
   795  	ut.testUpdaterNoOrder(tasks, false, []progress.Progress{
   796  		{ID: "a", Action: "preparing", Current: 6, Total: 10, HideCounts: true},
   797  		{ID: "b", Action: "running  ", Current: 9, Total: 10, HideCounts: true},
   798  		{ID: "c", Action: "accepted ", Current: 5, Total: 10, HideCounts: true},
   799  		{ID: "job progress", Action: "0 out of 3 complete", Current: 0, Total: 3, HideCounts: true},
   800  	})
   801  
   802  	tasks[2].Status.State = swarm.TaskStateRunning
   803  	tasks[3].Status.State = swarm.TaskStateComplete
   804  	tasks[4].Status.State = swarm.TaskStateRunning
   805  	ut.testUpdaterNoOrder(tasks, false, []progress.Progress{
   806  		{ID: "a", Action: "running  ", Current: 9, Total: 10, HideCounts: true},
   807  		{ID: "b", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   808  		{ID: "c", Action: "running  ", Current: 9, Total: 10, HideCounts: true},
   809  		{ID: "job progress", Action: "1 out of 3 complete", Current: 1, Total: 3, HideCounts: true},
   810  	})
   811  
   812  	tasks[2].Status.State = swarm.TaskStateFailed
   813  	tasks[2].Status.Err = "task failed"
   814  	tasks[4].Status.State = swarm.TaskStateComplete
   815  	ut.testUpdaterNoOrder(tasks, false, []progress.Progress{
   816  		{ID: "a", Action: "task failed"},
   817  		{ID: "b", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   818  		{ID: "c", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   819  		{ID: "job progress", Action: "2 out of 3 complete", Current: 2, Total: 3, HideCounts: true},
   820  	})
   821  
   822  	tasks = append(tasks, swarm.Task{
   823  		ID:           "task4",
   824  		DesiredState: swarm.TaskStateComplete,
   825  		Status:       swarm.TaskStatus{State: swarm.TaskStatePreparing},
   826  		NodeID:       tasks[2].NodeID,
   827  		JobIteration: &swarm.Version{Index: 1},
   828  	})
   829  
   830  	ut.testUpdaterNoOrder(tasks, false, []progress.Progress{
   831  		{ID: "a", Action: "preparing", Current: 6, Total: 10, HideCounts: true},
   832  		{ID: "b", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   833  		{ID: "c", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   834  		{ID: "job progress", Action: "2 out of 3 complete", Current: 2, Total: 3, HideCounts: true},
   835  	})
   836  
   837  	tasks[5].Status.State = swarm.TaskStateComplete
   838  	ut.testUpdaterNoOrder(tasks, true, []progress.Progress{
   839  		{ID: "a", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   840  		{ID: "b", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   841  		{ID: "c", Action: "complete ", Current: 10, Total: 10, HideCounts: true},
   842  		{ID: "job progress", Action: "3 out of 3 complete", Current: 3, Total: 3, HideCounts: true},
   843  	})
   844  }
   845  
   846  func TestGlobalJobProgressUpdaterLarge(t *testing.T) {
   847  	service := swarm.Service{
   848  		Spec: swarm.ServiceSpec{
   849  			Mode: swarm.ServiceMode{
   850  				GlobalJob: &swarm.GlobalJob{},
   851  			},
   852  		},
   853  		JobStatus: &swarm.JobStatus{
   854  			JobIteration: swarm.Version{Index: 1},
   855  		},
   856  	}
   857  
   858  	activeNodes := map[string]struct{}{}
   859  	for i := 0; i < 50; i++ {
   860  		activeNodes[fmt.Sprintf("node%v", i)] = struct{}{}
   861  	}
   862  
   863  	p := &mockProgress{}
   864  	ut := updaterTester{
   865  		t: t,
   866  		updater: &globalJobProgressUpdater{
   867  			progressOut: p,
   868  		},
   869  		p:           p,
   870  		activeNodes: activeNodes,
   871  		service:     service,
   872  	}
   873  
   874  	tasks := []swarm.Task{}
   875  	for nodeID := range activeNodes {
   876  		tasks = append(tasks, swarm.Task{
   877  			ID:           fmt.Sprintf("task%s", nodeID),
   878  			NodeID:       nodeID,
   879  			DesiredState: swarm.TaskStateComplete,
   880  			Status: swarm.TaskStatus{
   881  				State: swarm.TaskStateNew,
   882  			},
   883  			JobIteration: &swarm.Version{Index: 1},
   884  		})
   885  	}
   886  
   887  	// no bars, because too many tasks
   888  	ut.testUpdater(tasks, false, []progress.Progress{
   889  		{ID: "job progress", Action: " 0 out of 50 complete", Current: 0, Total: 50, HideCounts: true},
   890  		{ID: "job progress", Action: " 0 out of 50 complete", Current: 0, Total: 50, HideCounts: true},
   891  	})
   892  
   893  	for i := range tasks {
   894  		tasks[i].Status.State = swarm.TaskStateComplete
   895  		ut.testUpdater(tasks, i+1 == len(activeNodes), []progress.Progress{
   896  			{
   897  				ID:      "job progress",
   898  				Action:  fmt.Sprintf("%2d out of 50 complete", i+1),
   899  				Current: int64(i + 1), Total: 50, HideCounts: true,
   900  			},
   901  		})
   902  	}
   903  }