github.com/ranjib/nomad@v0.1.1-0.20160225204057-97751b02f70b/nomad/structs/structs_test.go (about)

     1  package structs
     2  
     3  import (
     4  	"reflect"
     5  	"strings"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/hashicorp/go-multierror"
    10  )
    11  
    12  func TestJob_Validate(t *testing.T) {
    13  	j := &Job{}
    14  	err := j.Validate()
    15  	mErr := err.(*multierror.Error)
    16  	if !strings.Contains(mErr.Errors[0].Error(), "job region") {
    17  		t.Fatalf("err: %s", err)
    18  	}
    19  	if !strings.Contains(mErr.Errors[1].Error(), "job ID") {
    20  		t.Fatalf("err: %s", err)
    21  	}
    22  	if !strings.Contains(mErr.Errors[2].Error(), "job name") {
    23  		t.Fatalf("err: %s", err)
    24  	}
    25  	if !strings.Contains(mErr.Errors[3].Error(), "job type") {
    26  		t.Fatalf("err: %s", err)
    27  	}
    28  	if !strings.Contains(mErr.Errors[4].Error(), "priority") {
    29  		t.Fatalf("err: %s", err)
    30  	}
    31  	if !strings.Contains(mErr.Errors[5].Error(), "datacenters") {
    32  		t.Fatalf("err: %s", err)
    33  	}
    34  	if !strings.Contains(mErr.Errors[6].Error(), "task groups") {
    35  		t.Fatalf("err: %s", err)
    36  	}
    37  
    38  	j = &Job{
    39  		Type: JobTypeService,
    40  		Periodic: &PeriodicConfig{
    41  			Enabled: true,
    42  		},
    43  	}
    44  	err = j.Validate()
    45  	mErr = err.(*multierror.Error)
    46  	if !strings.Contains(mErr.Error(), "Periodic") {
    47  		t.Fatalf("err: %s", err)
    48  	}
    49  
    50  	j = &Job{
    51  		Region:      "global",
    52  		ID:          GenerateUUID(),
    53  		Name:        "my-job",
    54  		Type:        JobTypeService,
    55  		Priority:    50,
    56  		Datacenters: []string{"dc1"},
    57  		TaskGroups: []*TaskGroup{
    58  			&TaskGroup{
    59  				Name: "web",
    60  				RestartPolicy: &RestartPolicy{
    61  					Interval: 5 * time.Minute,
    62  					Delay:    10 * time.Second,
    63  					Attempts: 10,
    64  				},
    65  			},
    66  			&TaskGroup{
    67  				Name: "web",
    68  				RestartPolicy: &RestartPolicy{
    69  					Interval: 5 * time.Minute,
    70  					Delay:    10 * time.Second,
    71  					Attempts: 10,
    72  				},
    73  			},
    74  			&TaskGroup{
    75  				RestartPolicy: &RestartPolicy{
    76  					Interval: 5 * time.Minute,
    77  					Delay:    10 * time.Second,
    78  					Attempts: 10,
    79  				},
    80  			},
    81  		},
    82  	}
    83  	err = j.Validate()
    84  	mErr = err.(*multierror.Error)
    85  	if !strings.Contains(mErr.Errors[0].Error(), "2 redefines 'web' from group 1") {
    86  		t.Fatalf("err: %s", err)
    87  	}
    88  	if !strings.Contains(mErr.Errors[1].Error(), "group 3 missing name") {
    89  		t.Fatalf("err: %s", err)
    90  	}
    91  	if !strings.Contains(mErr.Errors[2].Error(), "Task group 1 validation failed") {
    92  		t.Fatalf("err: %s", err)
    93  	}
    94  }
    95  
    96  func testJob() *Job {
    97  	return &Job{
    98  		Region:      "global",
    99  		ID:          GenerateUUID(),
   100  		Name:        "my-job",
   101  		Type:        JobTypeService,
   102  		Priority:    50,
   103  		AllAtOnce:   false,
   104  		Datacenters: []string{"dc1"},
   105  		Constraints: []*Constraint{
   106  			&Constraint{
   107  				LTarget: "$attr.kernel.name",
   108  				RTarget: "linux",
   109  				Operand: "=",
   110  			},
   111  		},
   112  		Periodic: &PeriodicConfig{
   113  			Enabled: false,
   114  		},
   115  		TaskGroups: []*TaskGroup{
   116  			&TaskGroup{
   117  				Name:  "web",
   118  				Count: 10,
   119  				RestartPolicy: &RestartPolicy{
   120  					Attempts: 3,
   121  					Interval: 10 * time.Minute,
   122  					Delay:    1 * time.Minute,
   123  				},
   124  				Tasks: []*Task{
   125  					&Task{
   126  						Name:   "web",
   127  						Driver: "exec",
   128  						Config: map[string]interface{}{
   129  							"command": "/bin/date",
   130  						},
   131  						Env: map[string]string{
   132  							"FOO": "bar",
   133  						},
   134  						Services: []*Service{
   135  							{
   136  								Name:      "${TASK}-frontend",
   137  								PortLabel: "http",
   138  							},
   139  						},
   140  						Resources: &Resources{
   141  							CPU:      500,
   142  							MemoryMB: 256,
   143  							Networks: []*NetworkResource{
   144  								&NetworkResource{
   145  									MBits:        50,
   146  									DynamicPorts: []Port{{Label: "http"}},
   147  								},
   148  							},
   149  						},
   150  					},
   151  				},
   152  				Meta: map[string]string{
   153  					"elb_check_type":     "http",
   154  					"elb_check_interval": "30s",
   155  					"elb_check_min":      "3",
   156  				},
   157  			},
   158  		},
   159  		Meta: map[string]string{
   160  			"owner": "armon",
   161  		},
   162  	}
   163  }
   164  
   165  func TestJob_Copy(t *testing.T) {
   166  	j := testJob()
   167  	c := j.Copy()
   168  	if !reflect.DeepEqual(j, c) {
   169  		t.Fatalf("Copy() returned an unequal Job; got %#v; want %#v", c, j)
   170  	}
   171  }
   172  
   173  func TestJob_IsPeriodic(t *testing.T) {
   174  	j := &Job{
   175  		Type: JobTypeService,
   176  		Periodic: &PeriodicConfig{
   177  			Enabled: true,
   178  		},
   179  	}
   180  	if !j.IsPeriodic() {
   181  		t.Fatalf("IsPeriodic() returned false on periodic job")
   182  	}
   183  
   184  	j = &Job{
   185  		Type: JobTypeService,
   186  	}
   187  	if j.IsPeriodic() {
   188  		t.Fatalf("IsPeriodic() returned true on non-periodic job")
   189  	}
   190  }
   191  
   192  func TestTaskGroup_Validate(t *testing.T) {
   193  	tg := &TaskGroup{
   194  		RestartPolicy: &RestartPolicy{
   195  			Interval: 5 * time.Minute,
   196  			Delay:    10 * time.Second,
   197  			Attempts: 10,
   198  			Mode:     RestartPolicyModeDelay,
   199  		},
   200  	}
   201  	err := tg.Validate()
   202  	mErr := err.(*multierror.Error)
   203  	if !strings.Contains(mErr.Errors[0].Error(), "group name") {
   204  		t.Fatalf("err: %s", err)
   205  	}
   206  	if !strings.Contains(mErr.Errors[1].Error(), "count must be positive") {
   207  		t.Fatalf("err: %s", err)
   208  	}
   209  	if !strings.Contains(mErr.Errors[2].Error(), "Missing tasks") {
   210  		t.Fatalf("err: %s", err)
   211  	}
   212  
   213  	tg = &TaskGroup{
   214  		Name:  "web",
   215  		Count: 1,
   216  		Tasks: []*Task{
   217  			&Task{Name: "web"},
   218  			&Task{Name: "web"},
   219  			&Task{},
   220  		},
   221  		RestartPolicy: &RestartPolicy{
   222  			Interval: 5 * time.Minute,
   223  			Delay:    10 * time.Second,
   224  			Attempts: 10,
   225  			Mode:     RestartPolicyModeDelay,
   226  		},
   227  	}
   228  	err = tg.Validate()
   229  	mErr = err.(*multierror.Error)
   230  	if !strings.Contains(mErr.Errors[0].Error(), "2 redefines 'web' from task 1") {
   231  		t.Fatalf("err: %s", err)
   232  	}
   233  	if !strings.Contains(mErr.Errors[1].Error(), "Task 3 missing name") {
   234  		t.Fatalf("err: %s", err)
   235  	}
   236  	if !strings.Contains(mErr.Errors[2].Error(), "Task 1 validation failed") {
   237  		t.Fatalf("err: %s", err)
   238  	}
   239  }
   240  
   241  func TestTask_Validate(t *testing.T) {
   242  	task := &Task{}
   243  	err := task.Validate()
   244  	mErr := err.(*multierror.Error)
   245  	if !strings.Contains(mErr.Errors[0].Error(), "task name") {
   246  		t.Fatalf("err: %s", err)
   247  	}
   248  	if !strings.Contains(mErr.Errors[1].Error(), "task driver") {
   249  		t.Fatalf("err: %s", err)
   250  	}
   251  	if !strings.Contains(mErr.Errors[2].Error(), "task resources") {
   252  		t.Fatalf("err: %s", err)
   253  	}
   254  
   255  	task = &Task{
   256  		Name:   "web",
   257  		Driver: "docker",
   258  		Resources: &Resources{
   259  			CPU:      100,
   260  			DiskMB:   200,
   261  			MemoryMB: 100,
   262  			IOPS:     10,
   263  		},
   264  		LogConfig: DefaultLogConfig(),
   265  	}
   266  	err = task.Validate()
   267  	if err != nil {
   268  		t.Fatalf("err: %s", err)
   269  	}
   270  }
   271  
   272  func TestTask_Validate_LogConfig(t *testing.T) {
   273  	task := &Task{
   274  		LogConfig: DefaultLogConfig(),
   275  		Resources: &Resources{
   276  			DiskMB: 1,
   277  		},
   278  	}
   279  
   280  	err := task.Validate()
   281  	mErr := err.(*multierror.Error)
   282  	if !strings.Contains(mErr.Errors[3].Error(), "log storage") {
   283  		t.Fatalf("err: %s", err)
   284  	}
   285  }
   286  
   287  func TestConstraint_Validate(t *testing.T) {
   288  	c := &Constraint{}
   289  	err := c.Validate()
   290  	mErr := err.(*multierror.Error)
   291  	if !strings.Contains(mErr.Errors[0].Error(), "Missing constraint operand") {
   292  		t.Fatalf("err: %s", err)
   293  	}
   294  
   295  	c = &Constraint{
   296  		LTarget: "$attr.kernel.name",
   297  		RTarget: "linux",
   298  		Operand: "=",
   299  	}
   300  	err = c.Validate()
   301  	if err != nil {
   302  		t.Fatalf("err: %v", err)
   303  	}
   304  
   305  	// Perform additional regexp validation
   306  	c.Operand = ConstraintRegex
   307  	c.RTarget = "(foo"
   308  	err = c.Validate()
   309  	mErr = err.(*multierror.Error)
   310  	if !strings.Contains(mErr.Errors[0].Error(), "missing closing") {
   311  		t.Fatalf("err: %s", err)
   312  	}
   313  
   314  	// Perform version validation
   315  	c.Operand = ConstraintVersion
   316  	c.RTarget = "~> foo"
   317  	err = c.Validate()
   318  	mErr = err.(*multierror.Error)
   319  	if !strings.Contains(mErr.Errors[0].Error(), "Malformed constraint") {
   320  		t.Fatalf("err: %s", err)
   321  	}
   322  }
   323  
   324  func TestResource_NetIndex(t *testing.T) {
   325  	r := &Resources{
   326  		Networks: []*NetworkResource{
   327  			&NetworkResource{Device: "eth0"},
   328  			&NetworkResource{Device: "lo0"},
   329  			&NetworkResource{Device: ""},
   330  		},
   331  	}
   332  	if idx := r.NetIndex(&NetworkResource{Device: "eth0"}); idx != 0 {
   333  		t.Fatalf("Bad: %d", idx)
   334  	}
   335  	if idx := r.NetIndex(&NetworkResource{Device: "lo0"}); idx != 1 {
   336  		t.Fatalf("Bad: %d", idx)
   337  	}
   338  	if idx := r.NetIndex(&NetworkResource{Device: "eth1"}); idx != -1 {
   339  		t.Fatalf("Bad: %d", idx)
   340  	}
   341  }
   342  
   343  func TestResource_Superset(t *testing.T) {
   344  	r1 := &Resources{
   345  		CPU:      2000,
   346  		MemoryMB: 2048,
   347  		DiskMB:   10000,
   348  		IOPS:     100,
   349  	}
   350  	r2 := &Resources{
   351  		CPU:      2000,
   352  		MemoryMB: 1024,
   353  		DiskMB:   5000,
   354  		IOPS:     50,
   355  	}
   356  
   357  	if s, _ := r1.Superset(r1); !s {
   358  		t.Fatalf("bad")
   359  	}
   360  	if s, _ := r1.Superset(r2); !s {
   361  		t.Fatalf("bad")
   362  	}
   363  	if s, _ := r2.Superset(r1); s {
   364  		t.Fatalf("bad")
   365  	}
   366  	if s, _ := r2.Superset(r2); !s {
   367  		t.Fatalf("bad")
   368  	}
   369  }
   370  
   371  func TestResource_Add(t *testing.T) {
   372  	r1 := &Resources{
   373  		CPU:      2000,
   374  		MemoryMB: 2048,
   375  		DiskMB:   10000,
   376  		IOPS:     100,
   377  		Networks: []*NetworkResource{
   378  			&NetworkResource{
   379  				CIDR:          "10.0.0.0/8",
   380  				MBits:         100,
   381  				ReservedPorts: []Port{{"ssh", 22}},
   382  			},
   383  		},
   384  	}
   385  	r2 := &Resources{
   386  		CPU:      2000,
   387  		MemoryMB: 1024,
   388  		DiskMB:   5000,
   389  		IOPS:     50,
   390  		Networks: []*NetworkResource{
   391  			&NetworkResource{
   392  				IP:            "10.0.0.1",
   393  				MBits:         50,
   394  				ReservedPorts: []Port{{"web", 80}},
   395  			},
   396  		},
   397  	}
   398  
   399  	err := r1.Add(r2)
   400  	if err != nil {
   401  		t.Fatalf("Err: %v", err)
   402  	}
   403  
   404  	expect := &Resources{
   405  		CPU:      3000,
   406  		MemoryMB: 3072,
   407  		DiskMB:   15000,
   408  		IOPS:     150,
   409  		Networks: []*NetworkResource{
   410  			&NetworkResource{
   411  				CIDR:          "10.0.0.0/8",
   412  				MBits:         150,
   413  				ReservedPorts: []Port{{"ssh", 22}, {"web", 80}},
   414  			},
   415  		},
   416  	}
   417  
   418  	if !reflect.DeepEqual(expect.Networks, r1.Networks) {
   419  		t.Fatalf("bad: %#v %#v", expect, r1)
   420  	}
   421  }
   422  
   423  func TestResource_Add_Network(t *testing.T) {
   424  	r1 := &Resources{}
   425  	r2 := &Resources{
   426  		Networks: []*NetworkResource{
   427  			&NetworkResource{
   428  				MBits:        50,
   429  				DynamicPorts: []Port{{"http", 0}, {"https", 0}},
   430  			},
   431  		},
   432  	}
   433  	r3 := &Resources{
   434  		Networks: []*NetworkResource{
   435  			&NetworkResource{
   436  				MBits:        25,
   437  				DynamicPorts: []Port{{"admin", 0}},
   438  			},
   439  		},
   440  	}
   441  
   442  	err := r1.Add(r2)
   443  	if err != nil {
   444  		t.Fatalf("Err: %v", err)
   445  	}
   446  	err = r1.Add(r3)
   447  	if err != nil {
   448  		t.Fatalf("Err: %v", err)
   449  	}
   450  
   451  	expect := &Resources{
   452  		Networks: []*NetworkResource{
   453  			&NetworkResource{
   454  				MBits:        75,
   455  				DynamicPorts: []Port{{"http", 0}, {"https", 0}, {"admin", 0}},
   456  			},
   457  		},
   458  	}
   459  
   460  	if !reflect.DeepEqual(expect.Networks, r1.Networks) {
   461  		t.Fatalf("bad: %#v %#v", expect.Networks[0], r1.Networks[0])
   462  	}
   463  }
   464  
   465  func TestEncodeDecode(t *testing.T) {
   466  	type FooRequest struct {
   467  		Foo string
   468  		Bar int
   469  		Baz bool
   470  	}
   471  	arg := &FooRequest{
   472  		Foo: "test",
   473  		Bar: 42,
   474  		Baz: true,
   475  	}
   476  	buf, err := Encode(1, arg)
   477  	if err != nil {
   478  		t.Fatalf("err: %v", err)
   479  	}
   480  
   481  	var out FooRequest
   482  	err = Decode(buf[1:], &out)
   483  	if err != nil {
   484  		t.Fatalf("err: %v", err)
   485  	}
   486  
   487  	if !reflect.DeepEqual(arg, &out) {
   488  		t.Fatalf("bad: %#v %#v", arg, out)
   489  	}
   490  }
   491  
   492  func BenchmarkEncodeDecode(b *testing.B) {
   493  	job := testJob()
   494  
   495  	for i := 0; i < b.N; i++ {
   496  		buf, err := Encode(1, job)
   497  		if err != nil {
   498  			b.Fatalf("err: %v", err)
   499  		}
   500  
   501  		var out Job
   502  		err = Decode(buf[1:], &out)
   503  		if err != nil {
   504  			b.Fatalf("err: %v", err)
   505  		}
   506  	}
   507  }
   508  
   509  func TestInvalidServiceCheck(t *testing.T) {
   510  	s := Service{
   511  		Name:      "service-name",
   512  		PortLabel: "bar",
   513  		Checks: []*ServiceCheck{
   514  			{
   515  
   516  				Name: "check-name",
   517  				Type: "lol",
   518  			},
   519  		},
   520  	}
   521  	if err := s.Validate(); err == nil {
   522  		t.Fatalf("Service should be invalid")
   523  	}
   524  
   525  	s = Service{
   526  		Name:      "service.name",
   527  		PortLabel: "bar",
   528  	}
   529  	if err := s.Validate(); err == nil {
   530  		t.Fatalf("Service should be invalid: %v", err)
   531  	}
   532  }
   533  
   534  func TestDistinctCheckID(t *testing.T) {
   535  	c1 := ServiceCheck{
   536  		Name:     "web-health",
   537  		Type:     "http",
   538  		Path:     "/health",
   539  		Interval: 2 * time.Second,
   540  		Timeout:  3 * time.Second,
   541  	}
   542  	c2 := ServiceCheck{
   543  		Name:     "web-health",
   544  		Type:     "http",
   545  		Path:     "/health1",
   546  		Interval: 2 * time.Second,
   547  		Timeout:  3 * time.Second,
   548  	}
   549  
   550  	c3 := ServiceCheck{
   551  		Name:     "web-health",
   552  		Type:     "http",
   553  		Path:     "/health",
   554  		Interval: 4 * time.Second,
   555  		Timeout:  3 * time.Second,
   556  	}
   557  	serviceID := "123"
   558  	c1Hash := c1.Hash(serviceID)
   559  	c2Hash := c2.Hash(serviceID)
   560  	c3Hash := c3.Hash(serviceID)
   561  
   562  	if c1Hash == c2Hash || c1Hash == c3Hash || c3Hash == c2Hash {
   563  		t.Fatalf("Checks need to be uniq c1: %s, c2: %s, c3: %s", c1Hash, c2Hash, c3Hash)
   564  	}
   565  
   566  }
   567  
   568  func TestService_InitFields(t *testing.T) {
   569  	job := "example"
   570  	taskGroup := "cache"
   571  	task := "redis"
   572  
   573  	s := Service{
   574  		Name: "${TASK}-db",
   575  	}
   576  
   577  	s.InitFields(job, taskGroup, task)
   578  	if s.Name != "redis-db" {
   579  		t.Fatalf("Expected name: %v, Actual: %v", "redis-db", s.Name)
   580  	}
   581  
   582  	s.Name = "db"
   583  	s.InitFields(job, taskGroup, task)
   584  	if s.Name != "db" {
   585  		t.Fatalf("Expected name: %v, Actual: %v", "redis-db", s.Name)
   586  	}
   587  
   588  	s.Name = "${JOB}-${TASKGROUP}-${TASK}-db"
   589  	s.InitFields(job, taskGroup, task)
   590  	if s.Name != "example-cache-redis-db" {
   591  		t.Fatalf("Expected name: %v, Actual: %v", "expample-cache-redis-db", s.Name)
   592  	}
   593  
   594  	s.Name = "${BASE}-db"
   595  	s.InitFields(job, taskGroup, task)
   596  	if s.Name != "example-cache-redis-db" {
   597  		t.Fatalf("Expected name: %v, Actual: %v", "expample-cache-redis-db", s.Name)
   598  	}
   599  
   600  }
   601  
   602  func TestJob_ExpandServiceNames(t *testing.T) {
   603  	j := &Job{
   604  		Name: "my-job",
   605  		TaskGroups: []*TaskGroup{
   606  			&TaskGroup{
   607  				Name: "web",
   608  				Tasks: []*Task{
   609  					{
   610  						Name: "frontend",
   611  						Services: []*Service{
   612  							{
   613  								Name: "${BASE}-default",
   614  							},
   615  							{
   616  								Name: "jmx",
   617  							},
   618  						},
   619  					},
   620  				},
   621  			},
   622  			&TaskGroup{
   623  				Name: "admin",
   624  				Tasks: []*Task{
   625  					{
   626  						Name: "admin-web",
   627  					},
   628  				},
   629  			},
   630  		},
   631  	}
   632  
   633  	j.InitFields()
   634  
   635  	service1Name := j.TaskGroups[0].Tasks[0].Services[0].Name
   636  	if service1Name != "my-job-web-frontend-default" {
   637  		t.Fatalf("Expected Service Name: %s, Actual: %s", "my-job-web-frontend-default", service1Name)
   638  	}
   639  
   640  	service2Name := j.TaskGroups[0].Tasks[0].Services[1].Name
   641  	if service2Name != "jmx" {
   642  		t.Fatalf("Expected Service Name: %s, Actual: %s", "jmx", service2Name)
   643  	}
   644  
   645  }
   646  
   647  func TestPeriodicConfig_EnabledInvalid(t *testing.T) {
   648  	// Create a config that is enabled but with no interval specified.
   649  	p := &PeriodicConfig{Enabled: true}
   650  	if err := p.Validate(); err == nil {
   651  		t.Fatal("Enabled PeriodicConfig with no spec or type shouldn't be valid")
   652  	}
   653  
   654  	// Create a config that is enabled, with a spec but no type specified.
   655  	p = &PeriodicConfig{Enabled: true, Spec: "foo"}
   656  	if err := p.Validate(); err == nil {
   657  		t.Fatal("Enabled PeriodicConfig with no spec type shouldn't be valid")
   658  	}
   659  
   660  	// Create a config that is enabled, with a spec type but no spec specified.
   661  	p = &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron}
   662  	if err := p.Validate(); err == nil {
   663  		t.Fatal("Enabled PeriodicConfig with no spec shouldn't be valid")
   664  	}
   665  }
   666  
   667  func TestPeriodicConfig_InvalidCron(t *testing.T) {
   668  	specs := []string{"foo", "* *", "@foo"}
   669  	for _, spec := range specs {
   670  		p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec}
   671  		if err := p.Validate(); err == nil {
   672  			t.Fatal("Invalid cron spec")
   673  		}
   674  	}
   675  }
   676  
   677  func TestPeriodicConfig_ValidCron(t *testing.T) {
   678  	specs := []string{"0 0 29 2 *", "@hourly", "0 0-15 * * *"}
   679  	for _, spec := range specs {
   680  		p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec}
   681  		if err := p.Validate(); err != nil {
   682  			t.Fatal("Passed valid cron")
   683  		}
   684  	}
   685  }
   686  
   687  func TestPeriodicConfig_NextCron(t *testing.T) {
   688  	from := time.Date(2009, time.November, 10, 23, 22, 30, 0, time.UTC)
   689  	specs := []string{"0 0 29 2 * 1980", "*/5 * * * *"}
   690  	expected := []time.Time{time.Time{}, time.Date(2009, time.November, 10, 23, 25, 0, 0, time.UTC)}
   691  	for i, spec := range specs {
   692  		p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec}
   693  		n := p.Next(from)
   694  		if expected[i] != n {
   695  			t.Fatalf("Next(%v) returned %v; want %v", from, n, expected[i])
   696  		}
   697  	}
   698  }
   699  
   700  func TestRestartPolicy_Validate(t *testing.T) {
   701  	// Policy with acceptable restart options passes
   702  	p := &RestartPolicy{
   703  		Mode:     RestartPolicyModeFail,
   704  		Attempts: 0,
   705  	}
   706  	if err := p.Validate(); err != nil {
   707  		t.Fatalf("err: %v", err)
   708  	}
   709  
   710  	// Policy with ambiguous restart options fails
   711  	p = &RestartPolicy{
   712  		Mode:     RestartPolicyModeDelay,
   713  		Attempts: 0,
   714  	}
   715  	if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "ambiguous") {
   716  		t.Fatalf("expect ambiguity error, got: %v", err)
   717  	}
   718  
   719  	// Bad policy mode fails
   720  	p = &RestartPolicy{
   721  		Mode:     "nope",
   722  		Attempts: 1,
   723  	}
   724  	if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "mode") {
   725  		t.Fatalf("expect mode error, got: %v", err)
   726  	}
   727  
   728  	// Fails when attempts*delay does not fit inside interval
   729  	p = &RestartPolicy{
   730  		Mode:     RestartPolicyModeDelay,
   731  		Attempts: 3,
   732  		Delay:    5 * time.Second,
   733  		Interval: time.Second,
   734  	}
   735  	if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "can't restart") {
   736  		t.Fatalf("expect restart interval error, got: %v", err)
   737  	}
   738  }