github.com/hhrutter/nomad@v0.6.0-rc2.0.20170723054333-80c4b03f0705/nomad/structs/structs_test.go (about)

     1  package structs
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  	"strings"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/hashicorp/consul/api"
    11  	"github.com/hashicorp/go-multierror"
    12  	"github.com/kr/pretty"
    13  )
    14  
    15  func TestJob_Validate(t *testing.T) {
    16  	j := &Job{}
    17  	err := j.Validate()
    18  	mErr := err.(*multierror.Error)
    19  	if !strings.Contains(mErr.Errors[0].Error(), "job region") {
    20  		t.Fatalf("err: %s", err)
    21  	}
    22  	if !strings.Contains(mErr.Errors[1].Error(), "job ID") {
    23  		t.Fatalf("err: %s", err)
    24  	}
    25  	if !strings.Contains(mErr.Errors[2].Error(), "job name") {
    26  		t.Fatalf("err: %s", err)
    27  	}
    28  	if !strings.Contains(mErr.Errors[3].Error(), "job type") {
    29  		t.Fatalf("err: %s", err)
    30  	}
    31  	if !strings.Contains(mErr.Errors[4].Error(), "priority") {
    32  		t.Fatalf("err: %s", err)
    33  	}
    34  	if !strings.Contains(mErr.Errors[5].Error(), "datacenters") {
    35  		t.Fatalf("err: %s", err)
    36  	}
    37  	if !strings.Contains(mErr.Errors[6].Error(), "task groups") {
    38  		t.Fatalf("err: %s", err)
    39  	}
    40  
    41  	j = &Job{
    42  		Type: "invalid-job-type",
    43  	}
    44  	err = j.Validate()
    45  	if expected := `Invalid job type: "invalid-job-type"`; !strings.Contains(err.Error(), expected) {
    46  		t.Errorf("expected %s but found: %v", expected, err)
    47  	}
    48  
    49  	j = &Job{
    50  		Type: JobTypeService,
    51  		Periodic: &PeriodicConfig{
    52  			Enabled: true,
    53  		},
    54  	}
    55  	err = j.Validate()
    56  	mErr = err.(*multierror.Error)
    57  	if !strings.Contains(mErr.Error(), "Periodic") {
    58  		t.Fatalf("err: %s", err)
    59  	}
    60  
    61  	j = &Job{
    62  		Region:      "global",
    63  		ID:          GenerateUUID(),
    64  		Name:        "my-job",
    65  		Type:        JobTypeService,
    66  		Priority:    50,
    67  		Datacenters: []string{"dc1"},
    68  		TaskGroups: []*TaskGroup{
    69  			&TaskGroup{
    70  				Name: "web",
    71  				RestartPolicy: &RestartPolicy{
    72  					Interval: 5 * time.Minute,
    73  					Delay:    10 * time.Second,
    74  					Attempts: 10,
    75  				},
    76  			},
    77  			&TaskGroup{
    78  				Name: "web",
    79  				RestartPolicy: &RestartPolicy{
    80  					Interval: 5 * time.Minute,
    81  					Delay:    10 * time.Second,
    82  					Attempts: 10,
    83  				},
    84  			},
    85  			&TaskGroup{
    86  				RestartPolicy: &RestartPolicy{
    87  					Interval: 5 * time.Minute,
    88  					Delay:    10 * time.Second,
    89  					Attempts: 10,
    90  				},
    91  			},
    92  		},
    93  	}
    94  	err = j.Validate()
    95  	mErr = err.(*multierror.Error)
    96  	if !strings.Contains(mErr.Errors[0].Error(), "2 redefines 'web' from group 1") {
    97  		t.Fatalf("err: %s", err)
    98  	}
    99  	if !strings.Contains(mErr.Errors[1].Error(), "group 3 missing name") {
   100  		t.Fatalf("err: %s", err)
   101  	}
   102  	if !strings.Contains(mErr.Errors[2].Error(), "Task group web validation failed") {
   103  		t.Fatalf("err: %s", err)
   104  	}
   105  }
   106  
   107  func TestJob_Warnings(t *testing.T) {
   108  	cases := []struct {
   109  		Name     string
   110  		Job      *Job
   111  		Expected []string
   112  	}{
   113  		{
   114  			Name:     "Higher counts for update stanza",
   115  			Expected: []string{"max parallel count is greater"},
   116  			Job: &Job{
   117  				Type: JobTypeService,
   118  				TaskGroups: []*TaskGroup{
   119  					{
   120  						Name:  "foo",
   121  						Count: 2,
   122  						Update: &UpdateStrategy{
   123  							MaxParallel: 10,
   124  						},
   125  					},
   126  				},
   127  			},
   128  		},
   129  	}
   130  
   131  	for _, c := range cases {
   132  		t.Run(c.Name, func(t *testing.T) {
   133  			warnings := c.Job.Warnings()
   134  			if warnings == nil {
   135  				if len(c.Expected) == 0 {
   136  					return
   137  				} else {
   138  					t.Fatal("Got no warnings when they were expected")
   139  				}
   140  			}
   141  
   142  			a := warnings.Error()
   143  			for _, e := range c.Expected {
   144  				if !strings.Contains(a, e) {
   145  					t.Fatalf("Got warnings %q; didn't contain %q", a, e)
   146  				}
   147  			}
   148  		})
   149  	}
   150  }
   151  
   152  func TestJob_Canonicalize_Update(t *testing.T) {
   153  	cases := []struct {
   154  		Name     string
   155  		Job      *Job
   156  		Expected *Job
   157  		Warnings []string
   158  	}{
   159  		{
   160  			Name:     "One task group",
   161  			Warnings: []string{"conversion to new update stanza"},
   162  			Job: &Job{
   163  				Type: JobTypeService,
   164  				Update: UpdateStrategy{
   165  					MaxParallel: 2,
   166  					Stagger:     10 * time.Second,
   167  				},
   168  				TaskGroups: []*TaskGroup{
   169  					{
   170  						Name:  "foo",
   171  						Count: 2,
   172  					},
   173  				},
   174  			},
   175  			Expected: &Job{
   176  				Type: JobTypeService,
   177  				Update: UpdateStrategy{
   178  					MaxParallel: 2,
   179  					Stagger:     10 * time.Second,
   180  				},
   181  				TaskGroups: []*TaskGroup{
   182  					{
   183  						Name:          "foo",
   184  						Count:         2,
   185  						RestartPolicy: NewRestartPolicy(JobTypeService),
   186  						EphemeralDisk: DefaultEphemeralDisk(),
   187  						Update: &UpdateStrategy{
   188  							Stagger:         30 * time.Second,
   189  							MaxParallel:     2,
   190  							HealthCheck:     UpdateStrategyHealthCheck_Checks,
   191  							MinHealthyTime:  10 * time.Second,
   192  							HealthyDeadline: 5 * time.Minute,
   193  							AutoRevert:      false,
   194  							Canary:          0,
   195  						},
   196  					},
   197  				},
   198  			},
   199  		},
   200  		{
   201  			Name:     "One task group batch",
   202  			Warnings: []string{"Update stanza is disallowed for batch jobs"},
   203  			Job: &Job{
   204  				Type: JobTypeBatch,
   205  				Update: UpdateStrategy{
   206  					MaxParallel: 2,
   207  					Stagger:     10 * time.Second,
   208  				},
   209  				TaskGroups: []*TaskGroup{
   210  					{
   211  						Name:  "foo",
   212  						Count: 2,
   213  					},
   214  				},
   215  			},
   216  			Expected: &Job{
   217  				Type:   JobTypeBatch,
   218  				Update: UpdateStrategy{},
   219  				TaskGroups: []*TaskGroup{
   220  					{
   221  						Name:          "foo",
   222  						Count:         2,
   223  						RestartPolicy: NewRestartPolicy(JobTypeBatch),
   224  						EphemeralDisk: DefaultEphemeralDisk(),
   225  					},
   226  				},
   227  			},
   228  		},
   229  		{
   230  			Name:     "One task group batch - new spec",
   231  			Warnings: []string{"Update stanza is disallowed for batch jobs"},
   232  			Job: &Job{
   233  				Type: JobTypeBatch,
   234  				Update: UpdateStrategy{
   235  					Stagger:         2 * time.Second,
   236  					MaxParallel:     2,
   237  					Canary:          2,
   238  					MinHealthyTime:  2 * time.Second,
   239  					HealthyDeadline: 10 * time.Second,
   240  					HealthCheck:     UpdateStrategyHealthCheck_Checks,
   241  				},
   242  				TaskGroups: []*TaskGroup{
   243  					{
   244  						Name:  "foo",
   245  						Count: 2,
   246  						Update: &UpdateStrategy{
   247  							Stagger:         2 * time.Second,
   248  							MaxParallel:     2,
   249  							Canary:          2,
   250  							MinHealthyTime:  2 * time.Second,
   251  							HealthyDeadline: 10 * time.Second,
   252  							HealthCheck:     UpdateStrategyHealthCheck_Checks,
   253  						},
   254  					},
   255  				},
   256  			},
   257  			Expected: &Job{
   258  				Type:   JobTypeBatch,
   259  				Update: UpdateStrategy{},
   260  				TaskGroups: []*TaskGroup{
   261  					{
   262  						Name:          "foo",
   263  						Count:         2,
   264  						RestartPolicy: NewRestartPolicy(JobTypeBatch),
   265  						EphemeralDisk: DefaultEphemeralDisk(),
   266  					},
   267  				},
   268  			},
   269  		},
   270  		{
   271  			Name: "One task group service - new spec",
   272  			Job: &Job{
   273  				Type: JobTypeService,
   274  				Update: UpdateStrategy{
   275  					Stagger:         2 * time.Second,
   276  					MaxParallel:     2,
   277  					Canary:          2,
   278  					MinHealthyTime:  2 * time.Second,
   279  					HealthyDeadline: 10 * time.Second,
   280  					HealthCheck:     UpdateStrategyHealthCheck_Checks,
   281  				},
   282  				TaskGroups: []*TaskGroup{
   283  					{
   284  						Name:  "foo",
   285  						Count: 2,
   286  						Update: &UpdateStrategy{
   287  							Stagger:         2 * time.Second,
   288  							MaxParallel:     2,
   289  							Canary:          2,
   290  							MinHealthyTime:  2 * time.Second,
   291  							HealthyDeadline: 10 * time.Second,
   292  							HealthCheck:     UpdateStrategyHealthCheck_Checks,
   293  						},
   294  					},
   295  				},
   296  			},
   297  			Expected: &Job{
   298  				Type: JobTypeService,
   299  				Update: UpdateStrategy{
   300  					Stagger:         2 * time.Second,
   301  					MaxParallel:     2,
   302  					Canary:          2,
   303  					MinHealthyTime:  2 * time.Second,
   304  					HealthyDeadline: 10 * time.Second,
   305  					HealthCheck:     UpdateStrategyHealthCheck_Checks,
   306  				},
   307  				TaskGroups: []*TaskGroup{
   308  					{
   309  						Name:          "foo",
   310  						Count:         2,
   311  						RestartPolicy: NewRestartPolicy(JobTypeService),
   312  						EphemeralDisk: DefaultEphemeralDisk(),
   313  						Update: &UpdateStrategy{
   314  							Stagger:         2 * time.Second,
   315  							MaxParallel:     2,
   316  							Canary:          2,
   317  							MinHealthyTime:  2 * time.Second,
   318  							HealthyDeadline: 10 * time.Second,
   319  							HealthCheck:     UpdateStrategyHealthCheck_Checks,
   320  						},
   321  					},
   322  				},
   323  			},
   324  		},
   325  		{
   326  			Name:     "One task group; too high of parallelism",
   327  			Warnings: []string{"conversion to new update stanza"},
   328  			Job: &Job{
   329  				Type: JobTypeService,
   330  				Update: UpdateStrategy{
   331  					MaxParallel: 200,
   332  					Stagger:     10 * time.Second,
   333  				},
   334  				TaskGroups: []*TaskGroup{
   335  					{
   336  						Name:  "foo",
   337  						Count: 2,
   338  					},
   339  				},
   340  			},
   341  			Expected: &Job{
   342  				Type: JobTypeService,
   343  				Update: UpdateStrategy{
   344  					MaxParallel: 200,
   345  					Stagger:     10 * time.Second,
   346  				},
   347  				TaskGroups: []*TaskGroup{
   348  					{
   349  						Name:          "foo",
   350  						Count:         2,
   351  						RestartPolicy: NewRestartPolicy(JobTypeService),
   352  						EphemeralDisk: DefaultEphemeralDisk(),
   353  						Update: &UpdateStrategy{
   354  							Stagger:         30 * time.Second,
   355  							MaxParallel:     2,
   356  							HealthCheck:     UpdateStrategyHealthCheck_Checks,
   357  							MinHealthyTime:  10 * time.Second,
   358  							HealthyDeadline: 5 * time.Minute,
   359  							AutoRevert:      false,
   360  							Canary:          0,
   361  						},
   362  					},
   363  				},
   364  			},
   365  		},
   366  		{
   367  			Name:     "Multiple task group; rounding",
   368  			Warnings: []string{"conversion to new update stanza"},
   369  			Job: &Job{
   370  				Type: JobTypeService,
   371  				Update: UpdateStrategy{
   372  					MaxParallel: 2,
   373  					Stagger:     10 * time.Second,
   374  				},
   375  				TaskGroups: []*TaskGroup{
   376  					{
   377  						Name:  "foo",
   378  						Count: 2,
   379  					},
   380  					{
   381  						Name:  "bar",
   382  						Count: 14,
   383  					},
   384  					{
   385  						Name:  "foo",
   386  						Count: 26,
   387  					},
   388  				},
   389  			},
   390  			Expected: &Job{
   391  				Type: JobTypeService,
   392  				Update: UpdateStrategy{
   393  					MaxParallel: 2,
   394  					Stagger:     10 * time.Second,
   395  				},
   396  				TaskGroups: []*TaskGroup{
   397  					{
   398  						Name:          "foo",
   399  						Count:         2,
   400  						RestartPolicy: NewRestartPolicy(JobTypeService),
   401  						EphemeralDisk: DefaultEphemeralDisk(),
   402  						Update: &UpdateStrategy{
   403  							Stagger:         30 * time.Second,
   404  							MaxParallel:     1,
   405  							HealthCheck:     UpdateStrategyHealthCheck_Checks,
   406  							MinHealthyTime:  10 * time.Second,
   407  							HealthyDeadline: 5 * time.Minute,
   408  							AutoRevert:      false,
   409  							Canary:          0,
   410  						},
   411  					},
   412  					{
   413  						Name:          "bar",
   414  						Count:         14,
   415  						RestartPolicy: NewRestartPolicy(JobTypeService),
   416  						EphemeralDisk: DefaultEphemeralDisk(),
   417  						Update: &UpdateStrategy{
   418  							Stagger:         30 * time.Second,
   419  							MaxParallel:     1,
   420  							HealthCheck:     UpdateStrategyHealthCheck_Checks,
   421  							MinHealthyTime:  10 * time.Second,
   422  							HealthyDeadline: 5 * time.Minute,
   423  							AutoRevert:      false,
   424  							Canary:          0,
   425  						},
   426  					},
   427  					{
   428  						Name:          "foo",
   429  						Count:         26,
   430  						EphemeralDisk: DefaultEphemeralDisk(),
   431  						RestartPolicy: NewRestartPolicy(JobTypeService),
   432  						Update: &UpdateStrategy{
   433  							Stagger:         30 * time.Second,
   434  							MaxParallel:     3,
   435  							HealthCheck:     UpdateStrategyHealthCheck_Checks,
   436  							MinHealthyTime:  10 * time.Second,
   437  							HealthyDeadline: 5 * time.Minute,
   438  							AutoRevert:      false,
   439  							Canary:          0,
   440  						},
   441  					},
   442  				},
   443  			},
   444  		},
   445  	}
   446  
   447  	for _, c := range cases {
   448  		t.Run(c.Name, func(t *testing.T) {
   449  			warnings := c.Job.Canonicalize()
   450  			if !reflect.DeepEqual(c.Job, c.Expected) {
   451  				t.Fatalf("Diff %#v", pretty.Diff(c.Job, c.Expected))
   452  			}
   453  
   454  			wErr := ""
   455  			if warnings != nil {
   456  				wErr = warnings.Error()
   457  			}
   458  			for _, w := range c.Warnings {
   459  				if !strings.Contains(wErr, w) {
   460  					t.Fatalf("Wanted warning %q: got %q", w, wErr)
   461  				}
   462  			}
   463  
   464  			if len(c.Warnings) == 0 && warnings != nil {
   465  				t.Fatalf("Wanted no warnings: got %q", wErr)
   466  			}
   467  		})
   468  	}
   469  }
   470  
   471  func TestJob_SpecChanged(t *testing.T) {
   472  	// Get a base test job
   473  	base := testJob()
   474  
   475  	// Only modify the indexes/mutable state of the job
   476  	mutatedBase := base.Copy()
   477  	mutatedBase.Status = "foo"
   478  	mutatedBase.ModifyIndex = base.ModifyIndex + 100
   479  
   480  	// changed contains a spec change that should be detected
   481  	change := base.Copy()
   482  	change.Priority = 99
   483  
   484  	cases := []struct {
   485  		Name     string
   486  		Original *Job
   487  		New      *Job
   488  		Changed  bool
   489  	}{
   490  		{
   491  			Name:     "Same job except mutable indexes",
   492  			Changed:  false,
   493  			Original: base,
   494  			New:      mutatedBase,
   495  		},
   496  		{
   497  			Name:     "Different",
   498  			Changed:  true,
   499  			Original: base,
   500  			New:      change,
   501  		},
   502  	}
   503  
   504  	for _, c := range cases {
   505  		t.Run(c.Name, func(t *testing.T) {
   506  			if actual := c.Original.SpecChanged(c.New); actual != c.Changed {
   507  				t.Fatalf("SpecChanged() returned %v; want %v", actual, c.Changed)
   508  			}
   509  		})
   510  	}
   511  }
   512  
   513  func testJob() *Job {
   514  	return &Job{
   515  		Region:      "global",
   516  		ID:          GenerateUUID(),
   517  		Name:        "my-job",
   518  		Type:        JobTypeService,
   519  		Priority:    50,
   520  		AllAtOnce:   false,
   521  		Datacenters: []string{"dc1"},
   522  		Constraints: []*Constraint{
   523  			&Constraint{
   524  				LTarget: "$attr.kernel.name",
   525  				RTarget: "linux",
   526  				Operand: "=",
   527  			},
   528  		},
   529  		Periodic: &PeriodicConfig{
   530  			Enabled: false,
   531  		},
   532  		TaskGroups: []*TaskGroup{
   533  			&TaskGroup{
   534  				Name:          "web",
   535  				Count:         10,
   536  				EphemeralDisk: DefaultEphemeralDisk(),
   537  				RestartPolicy: &RestartPolicy{
   538  					Mode:     RestartPolicyModeFail,
   539  					Attempts: 3,
   540  					Interval: 10 * time.Minute,
   541  					Delay:    1 * time.Minute,
   542  				},
   543  				Tasks: []*Task{
   544  					&Task{
   545  						Name:   "web",
   546  						Driver: "exec",
   547  						Config: map[string]interface{}{
   548  							"command": "/bin/date",
   549  						},
   550  						Env: map[string]string{
   551  							"FOO": "bar",
   552  						},
   553  						Artifacts: []*TaskArtifact{
   554  							{
   555  								GetterSource: "http://foo.com",
   556  							},
   557  						},
   558  						Services: []*Service{
   559  							{
   560  								Name:      "${TASK}-frontend",
   561  								PortLabel: "http",
   562  							},
   563  						},
   564  						Resources: &Resources{
   565  							CPU:      500,
   566  							MemoryMB: 256,
   567  							Networks: []*NetworkResource{
   568  								&NetworkResource{
   569  									MBits:        50,
   570  									DynamicPorts: []Port{{Label: "http"}},
   571  								},
   572  							},
   573  						},
   574  						LogConfig: &LogConfig{
   575  							MaxFiles:      10,
   576  							MaxFileSizeMB: 1,
   577  						},
   578  					},
   579  				},
   580  				Meta: map[string]string{
   581  					"elb_check_type":     "http",
   582  					"elb_check_interval": "30s",
   583  					"elb_check_min":      "3",
   584  				},
   585  			},
   586  		},
   587  		Meta: map[string]string{
   588  			"owner": "armon",
   589  		},
   590  	}
   591  }
   592  
   593  func TestJob_Copy(t *testing.T) {
   594  	j := testJob()
   595  	c := j.Copy()
   596  	if !reflect.DeepEqual(j, c) {
   597  		t.Fatalf("Copy() returned an unequal Job; got %#v; want %#v", c, j)
   598  	}
   599  }
   600  
   601  func TestJob_IsPeriodic(t *testing.T) {
   602  	j := &Job{
   603  		Type: JobTypeService,
   604  		Periodic: &PeriodicConfig{
   605  			Enabled: true,
   606  		},
   607  	}
   608  	if !j.IsPeriodic() {
   609  		t.Fatalf("IsPeriodic() returned false on periodic job")
   610  	}
   611  
   612  	j = &Job{
   613  		Type: JobTypeService,
   614  	}
   615  	if j.IsPeriodic() {
   616  		t.Fatalf("IsPeriodic() returned true on non-periodic job")
   617  	}
   618  }
   619  
   620  func TestJob_SystemJob_Validate(t *testing.T) {
   621  	j := testJob()
   622  	j.Type = JobTypeSystem
   623  	j.Canonicalize()
   624  
   625  	err := j.Validate()
   626  	if err == nil || !strings.Contains(err.Error(), "exceed") {
   627  		t.Fatalf("expect error due to count")
   628  	}
   629  
   630  	j.TaskGroups[0].Count = 0
   631  	if err := j.Validate(); err != nil {
   632  		t.Fatalf("unexpected err: %v", err)
   633  	}
   634  
   635  	j.TaskGroups[0].Count = 1
   636  	if err := j.Validate(); err != nil {
   637  		t.Fatalf("unexpected err: %v", err)
   638  	}
   639  }
   640  
   641  func TestJob_VaultPolicies(t *testing.T) {
   642  	j0 := &Job{}
   643  	e0 := make(map[string]map[string]*Vault, 0)
   644  
   645  	vj1 := &Vault{
   646  		Policies: []string{
   647  			"p1",
   648  			"p2",
   649  		},
   650  	}
   651  	vj2 := &Vault{
   652  		Policies: []string{
   653  			"p3",
   654  			"p4",
   655  		},
   656  	}
   657  	vj3 := &Vault{
   658  		Policies: []string{
   659  			"p5",
   660  		},
   661  	}
   662  	j1 := &Job{
   663  		TaskGroups: []*TaskGroup{
   664  			&TaskGroup{
   665  				Name: "foo",
   666  				Tasks: []*Task{
   667  					&Task{
   668  						Name: "t1",
   669  					},
   670  					&Task{
   671  						Name:  "t2",
   672  						Vault: vj1,
   673  					},
   674  				},
   675  			},
   676  			&TaskGroup{
   677  				Name: "bar",
   678  				Tasks: []*Task{
   679  					&Task{
   680  						Name:  "t3",
   681  						Vault: vj2,
   682  					},
   683  					&Task{
   684  						Name:  "t4",
   685  						Vault: vj3,
   686  					},
   687  				},
   688  			},
   689  		},
   690  	}
   691  
   692  	e1 := map[string]map[string]*Vault{
   693  		"foo": map[string]*Vault{
   694  			"t2": vj1,
   695  		},
   696  		"bar": map[string]*Vault{
   697  			"t3": vj2,
   698  			"t4": vj3,
   699  		},
   700  	}
   701  
   702  	cases := []struct {
   703  		Job      *Job
   704  		Expected map[string]map[string]*Vault
   705  	}{
   706  		{
   707  			Job:      j0,
   708  			Expected: e0,
   709  		},
   710  		{
   711  			Job:      j1,
   712  			Expected: e1,
   713  		},
   714  	}
   715  
   716  	for i, c := range cases {
   717  		got := c.Job.VaultPolicies()
   718  		if !reflect.DeepEqual(got, c.Expected) {
   719  			t.Fatalf("case %d: got %#v; want %#v", i+1, got, c.Expected)
   720  		}
   721  	}
   722  }
   723  
   724  func TestJob_RequiredSignals(t *testing.T) {
   725  	j0 := &Job{}
   726  	e0 := make(map[string]map[string][]string, 0)
   727  
   728  	vj1 := &Vault{
   729  		Policies:   []string{"p1"},
   730  		ChangeMode: VaultChangeModeNoop,
   731  	}
   732  	vj2 := &Vault{
   733  		Policies:     []string{"p1"},
   734  		ChangeMode:   VaultChangeModeSignal,
   735  		ChangeSignal: "SIGUSR1",
   736  	}
   737  	tj1 := &Template{
   738  		SourcePath: "foo",
   739  		DestPath:   "bar",
   740  		ChangeMode: TemplateChangeModeNoop,
   741  	}
   742  	tj2 := &Template{
   743  		SourcePath:   "foo",
   744  		DestPath:     "bar",
   745  		ChangeMode:   TemplateChangeModeSignal,
   746  		ChangeSignal: "SIGUSR2",
   747  	}
   748  	j1 := &Job{
   749  		TaskGroups: []*TaskGroup{
   750  			&TaskGroup{
   751  				Name: "foo",
   752  				Tasks: []*Task{
   753  					&Task{
   754  						Name: "t1",
   755  					},
   756  					&Task{
   757  						Name:      "t2",
   758  						Vault:     vj2,
   759  						Templates: []*Template{tj2},
   760  					},
   761  				},
   762  			},
   763  			&TaskGroup{
   764  				Name: "bar",
   765  				Tasks: []*Task{
   766  					&Task{
   767  						Name:      "t3",
   768  						Vault:     vj1,
   769  						Templates: []*Template{tj1},
   770  					},
   771  					&Task{
   772  						Name:  "t4",
   773  						Vault: vj2,
   774  					},
   775  				},
   776  			},
   777  		},
   778  	}
   779  
   780  	e1 := map[string]map[string][]string{
   781  		"foo": map[string][]string{
   782  			"t2": []string{"SIGUSR1", "SIGUSR2"},
   783  		},
   784  		"bar": map[string][]string{
   785  			"t4": []string{"SIGUSR1"},
   786  		},
   787  	}
   788  
   789  	cases := []struct {
   790  		Job      *Job
   791  		Expected map[string]map[string][]string
   792  	}{
   793  		{
   794  			Job:      j0,
   795  			Expected: e0,
   796  		},
   797  		{
   798  			Job:      j1,
   799  			Expected: e1,
   800  		},
   801  	}
   802  
   803  	for i, c := range cases {
   804  		got := c.Job.RequiredSignals()
   805  		if !reflect.DeepEqual(got, c.Expected) {
   806  			t.Fatalf("case %d: got %#v; want %#v", i+1, got, c.Expected)
   807  		}
   808  	}
   809  }
   810  
   811  func TestTaskGroup_Validate(t *testing.T) {
   812  	j := testJob()
   813  	tg := &TaskGroup{
   814  		Count: -1,
   815  		RestartPolicy: &RestartPolicy{
   816  			Interval: 5 * time.Minute,
   817  			Delay:    10 * time.Second,
   818  			Attempts: 10,
   819  			Mode:     RestartPolicyModeDelay,
   820  		},
   821  	}
   822  	err := tg.Validate(j)
   823  	mErr := err.(*multierror.Error)
   824  	if !strings.Contains(mErr.Errors[0].Error(), "group name") {
   825  		t.Fatalf("err: %s", err)
   826  	}
   827  	if !strings.Contains(mErr.Errors[1].Error(), "count can't be negative") {
   828  		t.Fatalf("err: %s", err)
   829  	}
   830  	if !strings.Contains(mErr.Errors[2].Error(), "Missing tasks") {
   831  		t.Fatalf("err: %s", err)
   832  	}
   833  
   834  	tg = &TaskGroup{
   835  		Tasks: []*Task{
   836  			&Task{
   837  				Name: "task-a",
   838  				Resources: &Resources{
   839  					Networks: []*NetworkResource{
   840  						&NetworkResource{
   841  							ReservedPorts: []Port{{Label: "foo", Value: 123}},
   842  						},
   843  					},
   844  				},
   845  			},
   846  			&Task{
   847  				Name: "task-b",
   848  				Resources: &Resources{
   849  					Networks: []*NetworkResource{
   850  						&NetworkResource{
   851  							ReservedPorts: []Port{{Label: "foo", Value: 123}},
   852  						},
   853  					},
   854  				},
   855  			},
   856  		},
   857  	}
   858  	err = tg.Validate(&Job{})
   859  	expected := `Static port 123 already reserved by task-a:foo`
   860  	if !strings.Contains(err.Error(), expected) {
   861  		t.Errorf("expected %s but found: %v", expected, err)
   862  	}
   863  
   864  	tg = &TaskGroup{
   865  		Tasks: []*Task{
   866  			&Task{
   867  				Name: "task-a",
   868  				Resources: &Resources{
   869  					Networks: []*NetworkResource{
   870  						&NetworkResource{
   871  							ReservedPorts: []Port{
   872  								{Label: "foo", Value: 123},
   873  								{Label: "bar", Value: 123},
   874  							},
   875  						},
   876  					},
   877  				},
   878  			},
   879  		},
   880  	}
   881  	err = tg.Validate(&Job{})
   882  	expected = `Static port 123 already reserved by task-a:foo`
   883  	if !strings.Contains(err.Error(), expected) {
   884  		t.Errorf("expected %s but found: %v", expected, err)
   885  	}
   886  
   887  	tg = &TaskGroup{
   888  		Name:  "web",
   889  		Count: 1,
   890  		Tasks: []*Task{
   891  			&Task{Name: "web", Leader: true},
   892  			&Task{Name: "web", Leader: true},
   893  			&Task{},
   894  		},
   895  		RestartPolicy: &RestartPolicy{
   896  			Interval: 5 * time.Minute,
   897  			Delay:    10 * time.Second,
   898  			Attempts: 10,
   899  			Mode:     RestartPolicyModeDelay,
   900  		},
   901  	}
   902  
   903  	err = tg.Validate(j)
   904  	mErr = err.(*multierror.Error)
   905  	if !strings.Contains(mErr.Errors[0].Error(), "should have an ephemeral disk object") {
   906  		t.Fatalf("err: %s", err)
   907  	}
   908  	if !strings.Contains(mErr.Errors[1].Error(), "2 redefines 'web' from task 1") {
   909  		t.Fatalf("err: %s", err)
   910  	}
   911  	if !strings.Contains(mErr.Errors[2].Error(), "Task 3 missing name") {
   912  		t.Fatalf("err: %s", err)
   913  	}
   914  	if !strings.Contains(mErr.Errors[3].Error(), "Only one task may be marked as leader") {
   915  		t.Fatalf("err: %s", err)
   916  	}
   917  	if !strings.Contains(mErr.Errors[4].Error(), "Task web validation failed") {
   918  		t.Fatalf("err: %s", err)
   919  	}
   920  
   921  	// COMPAT: Enable in 0.7.0
   922  	//j.Type = JobTypeBatch
   923  	//err = tg.Validate(j)
   924  	//if !strings.Contains(err.Error(), "does not allow update block") {
   925  	//t.Fatalf("err: %s", err)
   926  	//}
   927  }
   928  
   929  func TestTask_Validate(t *testing.T) {
   930  	task := &Task{}
   931  	ephemeralDisk := DefaultEphemeralDisk()
   932  	err := task.Validate(ephemeralDisk)
   933  	mErr := err.(*multierror.Error)
   934  	if !strings.Contains(mErr.Errors[0].Error(), "task name") {
   935  		t.Fatalf("err: %s", err)
   936  	}
   937  	if !strings.Contains(mErr.Errors[1].Error(), "task driver") {
   938  		t.Fatalf("err: %s", err)
   939  	}
   940  	if !strings.Contains(mErr.Errors[2].Error(), "task resources") {
   941  		t.Fatalf("err: %s", err)
   942  	}
   943  
   944  	task = &Task{Name: "web/foo"}
   945  	err = task.Validate(ephemeralDisk)
   946  	mErr = err.(*multierror.Error)
   947  	if !strings.Contains(mErr.Errors[0].Error(), "slashes") {
   948  		t.Fatalf("err: %s", err)
   949  	}
   950  
   951  	task = &Task{
   952  		Name:   "web",
   953  		Driver: "docker",
   954  		Resources: &Resources{
   955  			CPU:      100,
   956  			MemoryMB: 100,
   957  			IOPS:     10,
   958  		},
   959  		LogConfig: DefaultLogConfig(),
   960  	}
   961  	ephemeralDisk.SizeMB = 200
   962  	err = task.Validate(ephemeralDisk)
   963  	if err != nil {
   964  		t.Fatalf("err: %s", err)
   965  	}
   966  
   967  	task.Constraints = append(task.Constraints,
   968  		&Constraint{
   969  			Operand: ConstraintDistinctHosts,
   970  		},
   971  		&Constraint{
   972  			Operand: ConstraintDistinctProperty,
   973  			LTarget: "${meta.rack}",
   974  		})
   975  
   976  	err = task.Validate(ephemeralDisk)
   977  	mErr = err.(*multierror.Error)
   978  	if !strings.Contains(mErr.Errors[0].Error(), "task level: distinct_hosts") {
   979  		t.Fatalf("err: %s", err)
   980  	}
   981  	if !strings.Contains(mErr.Errors[1].Error(), "task level: distinct_property") {
   982  		t.Fatalf("err: %s", err)
   983  	}
   984  }
   985  
   986  func TestTask_Validate_Services(t *testing.T) {
   987  	s1 := &Service{
   988  		Name:      "service-name",
   989  		PortLabel: "bar",
   990  		Checks: []*ServiceCheck{
   991  			{
   992  				Name:     "check-name",
   993  				Type:     ServiceCheckTCP,
   994  				Interval: 0 * time.Second,
   995  			},
   996  			{
   997  				Name:    "check-name",
   998  				Type:    ServiceCheckTCP,
   999  				Timeout: 2 * time.Second,
  1000  			},
  1001  			{
  1002  				Name:     "check-name",
  1003  				Type:     ServiceCheckTCP,
  1004  				Interval: 1 * time.Second,
  1005  			},
  1006  		},
  1007  	}
  1008  
  1009  	s2 := &Service{
  1010  		Name:      "service-name",
  1011  		PortLabel: "bar",
  1012  	}
  1013  
  1014  	s3 := &Service{
  1015  		Name:      "service-A",
  1016  		PortLabel: "a",
  1017  	}
  1018  	s4 := &Service{
  1019  		Name:      "service-A",
  1020  		PortLabel: "b",
  1021  	}
  1022  
  1023  	ephemeralDisk := DefaultEphemeralDisk()
  1024  	ephemeralDisk.SizeMB = 200
  1025  	task := &Task{
  1026  		Name:   "web",
  1027  		Driver: "docker",
  1028  		Resources: &Resources{
  1029  			CPU:      100,
  1030  			MemoryMB: 100,
  1031  			IOPS:     10,
  1032  		},
  1033  		Services: []*Service{s1, s2},
  1034  	}
  1035  
  1036  	task1 := &Task{
  1037  		Name:      "web",
  1038  		Driver:    "docker",
  1039  		Resources: DefaultResources(),
  1040  		Services:  []*Service{s3, s4},
  1041  		LogConfig: DefaultLogConfig(),
  1042  	}
  1043  	task1.Resources.Networks = []*NetworkResource{
  1044  		&NetworkResource{
  1045  			MBits: 10,
  1046  			DynamicPorts: []Port{
  1047  				Port{
  1048  					Label: "a",
  1049  					Value: 1000,
  1050  				},
  1051  				Port{
  1052  					Label: "b",
  1053  					Value: 2000,
  1054  				},
  1055  			},
  1056  		},
  1057  	}
  1058  
  1059  	err := task.Validate(ephemeralDisk)
  1060  	if err == nil {
  1061  		t.Fatal("expected an error")
  1062  	}
  1063  
  1064  	if !strings.Contains(err.Error(), "service \"service-name\" is duplicate") {
  1065  		t.Fatalf("err: %v", err)
  1066  	}
  1067  
  1068  	if !strings.Contains(err.Error(), "check \"check-name\" is duplicate") {
  1069  		t.Fatalf("err: %v", err)
  1070  	}
  1071  
  1072  	if !strings.Contains(err.Error(), "missing required value interval") {
  1073  		t.Fatalf("err: %v", err)
  1074  	}
  1075  
  1076  	if !strings.Contains(err.Error(), "cannot be less than") {
  1077  		t.Fatalf("err: %v", err)
  1078  	}
  1079  
  1080  	if err = task1.Validate(ephemeralDisk); err != nil {
  1081  		t.Fatalf("err : %v", err)
  1082  	}
  1083  }
  1084  
  1085  func TestTask_Validate_Service_Check(t *testing.T) {
  1086  
  1087  	check1 := ServiceCheck{
  1088  		Name:     "check-name",
  1089  		Type:     ServiceCheckTCP,
  1090  		Interval: 10 * time.Second,
  1091  		Timeout:  2 * time.Second,
  1092  	}
  1093  
  1094  	err := check1.validate()
  1095  	if err != nil {
  1096  		t.Fatalf("err: %v", err)
  1097  	}
  1098  
  1099  	check1.InitialStatus = "foo"
  1100  	err = check1.validate()
  1101  	if err == nil {
  1102  		t.Fatal("Expected an error")
  1103  	}
  1104  
  1105  	if !strings.Contains(err.Error(), "invalid initial check state (foo)") {
  1106  		t.Fatalf("err: %v", err)
  1107  	}
  1108  
  1109  	check1.InitialStatus = api.HealthCritical
  1110  	err = check1.validate()
  1111  	if err != nil {
  1112  		t.Fatalf("err: %v", err)
  1113  	}
  1114  
  1115  	check1.InitialStatus = api.HealthPassing
  1116  	err = check1.validate()
  1117  	if err != nil {
  1118  		t.Fatalf("err: %v", err)
  1119  	}
  1120  
  1121  	check1.InitialStatus = ""
  1122  	err = check1.validate()
  1123  	if err != nil {
  1124  		t.Fatalf("err: %v", err)
  1125  	}
  1126  }
  1127  
  1128  func TestTask_Validate_LogConfig(t *testing.T) {
  1129  	task := &Task{
  1130  		LogConfig: DefaultLogConfig(),
  1131  	}
  1132  	ephemeralDisk := &EphemeralDisk{
  1133  		SizeMB: 1,
  1134  	}
  1135  
  1136  	err := task.Validate(ephemeralDisk)
  1137  	mErr := err.(*multierror.Error)
  1138  	if !strings.Contains(mErr.Errors[3].Error(), "log storage") {
  1139  		t.Fatalf("err: %s", err)
  1140  	}
  1141  }
  1142  
  1143  func TestTask_Validate_Template(t *testing.T) {
  1144  
  1145  	bad := &Template{}
  1146  	task := &Task{
  1147  		Templates: []*Template{bad},
  1148  	}
  1149  	ephemeralDisk := &EphemeralDisk{
  1150  		SizeMB: 1,
  1151  	}
  1152  
  1153  	err := task.Validate(ephemeralDisk)
  1154  	if !strings.Contains(err.Error(), "Template 1 validation failed") {
  1155  		t.Fatalf("err: %s", err)
  1156  	}
  1157  
  1158  	// Have two templates that share the same destination
  1159  	good := &Template{
  1160  		SourcePath: "foo",
  1161  		DestPath:   "local/foo",
  1162  		ChangeMode: "noop",
  1163  	}
  1164  
  1165  	task.Templates = []*Template{good, good}
  1166  	err = task.Validate(ephemeralDisk)
  1167  	if !strings.Contains(err.Error(), "same destination as") {
  1168  		t.Fatalf("err: %s", err)
  1169  	}
  1170  }
  1171  
  1172  func TestTemplate_Validate(t *testing.T) {
  1173  	cases := []struct {
  1174  		Tmpl         *Template
  1175  		Fail         bool
  1176  		ContainsErrs []string
  1177  	}{
  1178  		{
  1179  			Tmpl: &Template{},
  1180  			Fail: true,
  1181  			ContainsErrs: []string{
  1182  				"specify a source path",
  1183  				"specify a destination",
  1184  				TemplateChangeModeInvalidError.Error(),
  1185  			},
  1186  		},
  1187  		{
  1188  			Tmpl: &Template{
  1189  				Splay: -100,
  1190  			},
  1191  			Fail: true,
  1192  			ContainsErrs: []string{
  1193  				"positive splay",
  1194  			},
  1195  		},
  1196  		{
  1197  			Tmpl: &Template{
  1198  				ChangeMode: "foo",
  1199  			},
  1200  			Fail: true,
  1201  			ContainsErrs: []string{
  1202  				TemplateChangeModeInvalidError.Error(),
  1203  			},
  1204  		},
  1205  		{
  1206  			Tmpl: &Template{
  1207  				ChangeMode: "signal",
  1208  			},
  1209  			Fail: true,
  1210  			ContainsErrs: []string{
  1211  				"specify signal value",
  1212  			},
  1213  		},
  1214  		{
  1215  			Tmpl: &Template{
  1216  				SourcePath: "foo",
  1217  				DestPath:   "../../root",
  1218  				ChangeMode: "noop",
  1219  			},
  1220  			Fail: true,
  1221  			ContainsErrs: []string{
  1222  				"destination escapes",
  1223  			},
  1224  		},
  1225  		{
  1226  			Tmpl: &Template{
  1227  				SourcePath: "foo",
  1228  				DestPath:   "local/foo",
  1229  				ChangeMode: "noop",
  1230  			},
  1231  			Fail: false,
  1232  		},
  1233  		{
  1234  			Tmpl: &Template{
  1235  				SourcePath: "foo",
  1236  				DestPath:   "local/foo",
  1237  				ChangeMode: "noop",
  1238  				Perms:      "0444",
  1239  			},
  1240  			Fail: false,
  1241  		},
  1242  		{
  1243  			Tmpl: &Template{
  1244  				SourcePath: "foo",
  1245  				DestPath:   "local/foo",
  1246  				ChangeMode: "noop",
  1247  				Perms:      "zza",
  1248  			},
  1249  			Fail: true,
  1250  			ContainsErrs: []string{
  1251  				"as octal",
  1252  			},
  1253  		},
  1254  	}
  1255  
  1256  	for i, c := range cases {
  1257  		err := c.Tmpl.Validate()
  1258  		if err != nil {
  1259  			if !c.Fail {
  1260  				t.Fatalf("Case %d: shouldn't have failed: %v", i+1, err)
  1261  			}
  1262  
  1263  			e := err.Error()
  1264  			for _, exp := range c.ContainsErrs {
  1265  				if !strings.Contains(e, exp) {
  1266  					t.Fatalf("Cased %d: should have contained error %q: %q", i+1, exp, e)
  1267  				}
  1268  			}
  1269  		} else if c.Fail {
  1270  			t.Fatalf("Case %d: should have failed: %v", i+1, err)
  1271  		}
  1272  	}
  1273  }
  1274  
  1275  func TestConstraint_Validate(t *testing.T) {
  1276  	c := &Constraint{}
  1277  	err := c.Validate()
  1278  	mErr := err.(*multierror.Error)
  1279  	if !strings.Contains(mErr.Errors[0].Error(), "Missing constraint operand") {
  1280  		t.Fatalf("err: %s", err)
  1281  	}
  1282  
  1283  	c = &Constraint{
  1284  		LTarget: "$attr.kernel.name",
  1285  		RTarget: "linux",
  1286  		Operand: "=",
  1287  	}
  1288  	err = c.Validate()
  1289  	if err != nil {
  1290  		t.Fatalf("err: %v", err)
  1291  	}
  1292  
  1293  	// Perform additional regexp validation
  1294  	c.Operand = ConstraintRegex
  1295  	c.RTarget = "(foo"
  1296  	err = c.Validate()
  1297  	mErr = err.(*multierror.Error)
  1298  	if !strings.Contains(mErr.Errors[0].Error(), "missing closing") {
  1299  		t.Fatalf("err: %s", err)
  1300  	}
  1301  
  1302  	// Perform version validation
  1303  	c.Operand = ConstraintVersion
  1304  	c.RTarget = "~> foo"
  1305  	err = c.Validate()
  1306  	mErr = err.(*multierror.Error)
  1307  	if !strings.Contains(mErr.Errors[0].Error(), "Malformed constraint") {
  1308  		t.Fatalf("err: %s", err)
  1309  	}
  1310  }
  1311  
  1312  func TestUpdateStrategy_Validate(t *testing.T) {
  1313  	u := &UpdateStrategy{
  1314  		MaxParallel:     -1,
  1315  		HealthCheck:     "foo",
  1316  		MinHealthyTime:  -10,
  1317  		HealthyDeadline: -10,
  1318  		AutoRevert:      false,
  1319  		Canary:          -1,
  1320  	}
  1321  
  1322  	err := u.Validate()
  1323  	mErr := err.(*multierror.Error)
  1324  	if !strings.Contains(mErr.Errors[0].Error(), "Invalid health check given") {
  1325  		t.Fatalf("err: %s", err)
  1326  	}
  1327  	if !strings.Contains(mErr.Errors[1].Error(), "Max parallel can not be less than zero") {
  1328  		t.Fatalf("err: %s", err)
  1329  	}
  1330  	if !strings.Contains(mErr.Errors[2].Error(), "Canary count can not be less than zero") {
  1331  		t.Fatalf("err: %s", err)
  1332  	}
  1333  	if !strings.Contains(mErr.Errors[3].Error(), "Minimum healthy time may not be less than zero") {
  1334  		t.Fatalf("err: %s", err)
  1335  	}
  1336  	if !strings.Contains(mErr.Errors[4].Error(), "Healthy deadline must be greater than zero") {
  1337  		t.Fatalf("err: %s", err)
  1338  	}
  1339  }
  1340  
  1341  func TestResource_NetIndex(t *testing.T) {
  1342  	r := &Resources{
  1343  		Networks: []*NetworkResource{
  1344  			&NetworkResource{Device: "eth0"},
  1345  			&NetworkResource{Device: "lo0"},
  1346  			&NetworkResource{Device: ""},
  1347  		},
  1348  	}
  1349  	if idx := r.NetIndex(&NetworkResource{Device: "eth0"}); idx != 0 {
  1350  		t.Fatalf("Bad: %d", idx)
  1351  	}
  1352  	if idx := r.NetIndex(&NetworkResource{Device: "lo0"}); idx != 1 {
  1353  		t.Fatalf("Bad: %d", idx)
  1354  	}
  1355  	if idx := r.NetIndex(&NetworkResource{Device: "eth1"}); idx != -1 {
  1356  		t.Fatalf("Bad: %d", idx)
  1357  	}
  1358  }
  1359  
  1360  func TestResource_Superset(t *testing.T) {
  1361  	r1 := &Resources{
  1362  		CPU:      2000,
  1363  		MemoryMB: 2048,
  1364  		DiskMB:   10000,
  1365  		IOPS:     100,
  1366  	}
  1367  	r2 := &Resources{
  1368  		CPU:      2000,
  1369  		MemoryMB: 1024,
  1370  		DiskMB:   5000,
  1371  		IOPS:     50,
  1372  	}
  1373  
  1374  	if s, _ := r1.Superset(r1); !s {
  1375  		t.Fatalf("bad")
  1376  	}
  1377  	if s, _ := r1.Superset(r2); !s {
  1378  		t.Fatalf("bad")
  1379  	}
  1380  	if s, _ := r2.Superset(r1); s {
  1381  		t.Fatalf("bad")
  1382  	}
  1383  	if s, _ := r2.Superset(r2); !s {
  1384  		t.Fatalf("bad")
  1385  	}
  1386  }
  1387  
  1388  func TestResource_Add(t *testing.T) {
  1389  	r1 := &Resources{
  1390  		CPU:      2000,
  1391  		MemoryMB: 2048,
  1392  		DiskMB:   10000,
  1393  		IOPS:     100,
  1394  		Networks: []*NetworkResource{
  1395  			&NetworkResource{
  1396  				CIDR:          "10.0.0.0/8",
  1397  				MBits:         100,
  1398  				ReservedPorts: []Port{{"ssh", 22}},
  1399  			},
  1400  		},
  1401  	}
  1402  	r2 := &Resources{
  1403  		CPU:      2000,
  1404  		MemoryMB: 1024,
  1405  		DiskMB:   5000,
  1406  		IOPS:     50,
  1407  		Networks: []*NetworkResource{
  1408  			&NetworkResource{
  1409  				IP:            "10.0.0.1",
  1410  				MBits:         50,
  1411  				ReservedPorts: []Port{{"web", 80}},
  1412  			},
  1413  		},
  1414  	}
  1415  
  1416  	err := r1.Add(r2)
  1417  	if err != nil {
  1418  		t.Fatalf("Err: %v", err)
  1419  	}
  1420  
  1421  	expect := &Resources{
  1422  		CPU:      3000,
  1423  		MemoryMB: 3072,
  1424  		DiskMB:   15000,
  1425  		IOPS:     150,
  1426  		Networks: []*NetworkResource{
  1427  			&NetworkResource{
  1428  				CIDR:          "10.0.0.0/8",
  1429  				MBits:         150,
  1430  				ReservedPorts: []Port{{"ssh", 22}, {"web", 80}},
  1431  			},
  1432  		},
  1433  	}
  1434  
  1435  	if !reflect.DeepEqual(expect.Networks, r1.Networks) {
  1436  		t.Fatalf("bad: %#v %#v", expect, r1)
  1437  	}
  1438  }
  1439  
  1440  func TestResource_Add_Network(t *testing.T) {
  1441  	r1 := &Resources{}
  1442  	r2 := &Resources{
  1443  		Networks: []*NetworkResource{
  1444  			&NetworkResource{
  1445  				MBits:        50,
  1446  				DynamicPorts: []Port{{"http", 0}, {"https", 0}},
  1447  			},
  1448  		},
  1449  	}
  1450  	r3 := &Resources{
  1451  		Networks: []*NetworkResource{
  1452  			&NetworkResource{
  1453  				MBits:        25,
  1454  				DynamicPorts: []Port{{"admin", 0}},
  1455  			},
  1456  		},
  1457  	}
  1458  
  1459  	err := r1.Add(r2)
  1460  	if err != nil {
  1461  		t.Fatalf("Err: %v", err)
  1462  	}
  1463  	err = r1.Add(r3)
  1464  	if err != nil {
  1465  		t.Fatalf("Err: %v", err)
  1466  	}
  1467  
  1468  	expect := &Resources{
  1469  		Networks: []*NetworkResource{
  1470  			&NetworkResource{
  1471  				MBits:        75,
  1472  				DynamicPorts: []Port{{"http", 0}, {"https", 0}, {"admin", 0}},
  1473  			},
  1474  		},
  1475  	}
  1476  
  1477  	if !reflect.DeepEqual(expect.Networks, r1.Networks) {
  1478  		t.Fatalf("bad: %#v %#v", expect.Networks[0], r1.Networks[0])
  1479  	}
  1480  }
  1481  
  1482  func TestEncodeDecode(t *testing.T) {
  1483  	type FooRequest struct {
  1484  		Foo string
  1485  		Bar int
  1486  		Baz bool
  1487  	}
  1488  	arg := &FooRequest{
  1489  		Foo: "test",
  1490  		Bar: 42,
  1491  		Baz: true,
  1492  	}
  1493  	buf, err := Encode(1, arg)
  1494  	if err != nil {
  1495  		t.Fatalf("err: %v", err)
  1496  	}
  1497  
  1498  	var out FooRequest
  1499  	err = Decode(buf[1:], &out)
  1500  	if err != nil {
  1501  		t.Fatalf("err: %v", err)
  1502  	}
  1503  
  1504  	if !reflect.DeepEqual(arg, &out) {
  1505  		t.Fatalf("bad: %#v %#v", arg, out)
  1506  	}
  1507  }
  1508  
  1509  func BenchmarkEncodeDecode(b *testing.B) {
  1510  	job := testJob()
  1511  
  1512  	for i := 0; i < b.N; i++ {
  1513  		buf, err := Encode(1, job)
  1514  		if err != nil {
  1515  			b.Fatalf("err: %v", err)
  1516  		}
  1517  
  1518  		var out Job
  1519  		err = Decode(buf[1:], &out)
  1520  		if err != nil {
  1521  			b.Fatalf("err: %v", err)
  1522  		}
  1523  	}
  1524  }
  1525  
  1526  func TestInvalidServiceCheck(t *testing.T) {
  1527  	s := Service{
  1528  		Name:      "service-name",
  1529  		PortLabel: "bar",
  1530  		Checks: []*ServiceCheck{
  1531  			{
  1532  				Name: "check-name",
  1533  				Type: "lol",
  1534  			},
  1535  		},
  1536  	}
  1537  	if err := s.Validate(); err == nil {
  1538  		t.Fatalf("Service should be invalid (invalid type)")
  1539  	}
  1540  
  1541  	s = Service{
  1542  		Name:      "service.name",
  1543  		PortLabel: "bar",
  1544  	}
  1545  	if err := s.ValidateName(s.Name); err == nil {
  1546  		t.Fatalf("Service should be invalid (contains a dot): %v", err)
  1547  	}
  1548  
  1549  	s = Service{
  1550  		Name:      "-my-service",
  1551  		PortLabel: "bar",
  1552  	}
  1553  	if err := s.Validate(); err == nil {
  1554  		t.Fatalf("Service should be invalid (begins with a hyphen): %v", err)
  1555  	}
  1556  
  1557  	s = Service{
  1558  		Name:      "my-service-${NOMAD_META_FOO}",
  1559  		PortLabel: "bar",
  1560  	}
  1561  	if err := s.Validate(); err != nil {
  1562  		t.Fatalf("Service should be valid: %v", err)
  1563  	}
  1564  
  1565  	s = Service{
  1566  		Name:      "abcdef0123456789-abcdef0123456789-abcdef0123456789-abcdef0123456",
  1567  		PortLabel: "bar",
  1568  	}
  1569  	if err := s.ValidateName(s.Name); err == nil {
  1570  		t.Fatalf("Service should be invalid (too long): %v", err)
  1571  	}
  1572  
  1573  	s = Service{
  1574  		Name: "service-name",
  1575  		Checks: []*ServiceCheck{
  1576  			{
  1577  				Name:     "check-tcp",
  1578  				Type:     ServiceCheckTCP,
  1579  				Interval: 5 * time.Second,
  1580  				Timeout:  2 * time.Second,
  1581  			},
  1582  			{
  1583  				Name:     "check-http",
  1584  				Type:     ServiceCheckHTTP,
  1585  				Path:     "/foo",
  1586  				Interval: 5 * time.Second,
  1587  				Timeout:  2 * time.Second,
  1588  			},
  1589  		},
  1590  	}
  1591  	if err := s.Validate(); err == nil {
  1592  		t.Fatalf("service should be invalid (tcp/http checks with no port): %v", err)
  1593  	}
  1594  
  1595  	s = Service{
  1596  		Name: "service-name",
  1597  		Checks: []*ServiceCheck{
  1598  			{
  1599  				Name:     "check-script",
  1600  				Type:     ServiceCheckScript,
  1601  				Command:  "/bin/date",
  1602  				Interval: 5 * time.Second,
  1603  				Timeout:  2 * time.Second,
  1604  			},
  1605  		},
  1606  	}
  1607  	if err := s.Validate(); err != nil {
  1608  		t.Fatalf("un-expected error: %v", err)
  1609  	}
  1610  }
  1611  
  1612  func TestDistinctCheckID(t *testing.T) {
  1613  	c1 := ServiceCheck{
  1614  		Name:     "web-health",
  1615  		Type:     "http",
  1616  		Path:     "/health",
  1617  		Interval: 2 * time.Second,
  1618  		Timeout:  3 * time.Second,
  1619  	}
  1620  	c2 := ServiceCheck{
  1621  		Name:     "web-health",
  1622  		Type:     "http",
  1623  		Path:     "/health1",
  1624  		Interval: 2 * time.Second,
  1625  		Timeout:  3 * time.Second,
  1626  	}
  1627  
  1628  	c3 := ServiceCheck{
  1629  		Name:     "web-health",
  1630  		Type:     "http",
  1631  		Path:     "/health",
  1632  		Interval: 4 * time.Second,
  1633  		Timeout:  3 * time.Second,
  1634  	}
  1635  	serviceID := "123"
  1636  	c1Hash := c1.Hash(serviceID)
  1637  	c2Hash := c2.Hash(serviceID)
  1638  	c3Hash := c3.Hash(serviceID)
  1639  
  1640  	if c1Hash == c2Hash || c1Hash == c3Hash || c3Hash == c2Hash {
  1641  		t.Fatalf("Checks need to be uniq c1: %s, c2: %s, c3: %s", c1Hash, c2Hash, c3Hash)
  1642  	}
  1643  
  1644  }
  1645  
  1646  func TestService_Canonicalize(t *testing.T) {
  1647  	job := "example"
  1648  	taskGroup := "cache"
  1649  	task := "redis"
  1650  
  1651  	s := Service{
  1652  		Name: "${TASK}-db",
  1653  	}
  1654  
  1655  	s.Canonicalize(job, taskGroup, task)
  1656  	if s.Name != "redis-db" {
  1657  		t.Fatalf("Expected name: %v, Actual: %v", "redis-db", s.Name)
  1658  	}
  1659  
  1660  	s.Name = "db"
  1661  	s.Canonicalize(job, taskGroup, task)
  1662  	if s.Name != "db" {
  1663  		t.Fatalf("Expected name: %v, Actual: %v", "redis-db", s.Name)
  1664  	}
  1665  
  1666  	s.Name = "${JOB}-${TASKGROUP}-${TASK}-db"
  1667  	s.Canonicalize(job, taskGroup, task)
  1668  	if s.Name != "example-cache-redis-db" {
  1669  		t.Fatalf("Expected name: %v, Actual: %v", "expample-cache-redis-db", s.Name)
  1670  	}
  1671  
  1672  	s.Name = "${BASE}-db"
  1673  	s.Canonicalize(job, taskGroup, task)
  1674  	if s.Name != "example-cache-redis-db" {
  1675  		t.Fatalf("Expected name: %v, Actual: %v", "expample-cache-redis-db", s.Name)
  1676  	}
  1677  
  1678  }
  1679  
  1680  func TestJob_ExpandServiceNames(t *testing.T) {
  1681  	j := &Job{
  1682  		Name: "my-job",
  1683  		TaskGroups: []*TaskGroup{
  1684  			&TaskGroup{
  1685  				Name: "web",
  1686  				Tasks: []*Task{
  1687  					{
  1688  						Name: "frontend",
  1689  						Services: []*Service{
  1690  							{
  1691  								Name: "${BASE}-default",
  1692  							},
  1693  							{
  1694  								Name: "jmx",
  1695  							},
  1696  						},
  1697  					},
  1698  				},
  1699  			},
  1700  			&TaskGroup{
  1701  				Name: "admin",
  1702  				Tasks: []*Task{
  1703  					{
  1704  						Name: "admin-web",
  1705  					},
  1706  				},
  1707  			},
  1708  		},
  1709  	}
  1710  
  1711  	j.Canonicalize()
  1712  
  1713  	service1Name := j.TaskGroups[0].Tasks[0].Services[0].Name
  1714  	if service1Name != "my-job-web-frontend-default" {
  1715  		t.Fatalf("Expected Service Name: %s, Actual: %s", "my-job-web-frontend-default", service1Name)
  1716  	}
  1717  
  1718  	service2Name := j.TaskGroups[0].Tasks[0].Services[1].Name
  1719  	if service2Name != "jmx" {
  1720  		t.Fatalf("Expected Service Name: %s, Actual: %s", "jmx", service2Name)
  1721  	}
  1722  
  1723  }
  1724  
  1725  func TestPeriodicConfig_EnabledInvalid(t *testing.T) {
  1726  	// Create a config that is enabled but with no interval specified.
  1727  	p := &PeriodicConfig{Enabled: true}
  1728  	if err := p.Validate(); err == nil {
  1729  		t.Fatal("Enabled PeriodicConfig with no spec or type shouldn't be valid")
  1730  	}
  1731  
  1732  	// Create a config that is enabled, with a spec but no type specified.
  1733  	p = &PeriodicConfig{Enabled: true, Spec: "foo"}
  1734  	if err := p.Validate(); err == nil {
  1735  		t.Fatal("Enabled PeriodicConfig with no spec type shouldn't be valid")
  1736  	}
  1737  
  1738  	// Create a config that is enabled, with a spec type but no spec specified.
  1739  	p = &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron}
  1740  	if err := p.Validate(); err == nil {
  1741  		t.Fatal("Enabled PeriodicConfig with no spec shouldn't be valid")
  1742  	}
  1743  
  1744  	// Create a config that is enabled, with a bad time zone.
  1745  	p = &PeriodicConfig{Enabled: true, TimeZone: "FOO"}
  1746  	if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "time zone") {
  1747  		t.Fatalf("Enabled PeriodicConfig with bad time zone shouldn't be valid: %v", err)
  1748  	}
  1749  }
  1750  
  1751  func TestPeriodicConfig_InvalidCron(t *testing.T) {
  1752  	specs := []string{"foo", "* *", "@foo"}
  1753  	for _, spec := range specs {
  1754  		p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec}
  1755  		p.Canonicalize()
  1756  		if err := p.Validate(); err == nil {
  1757  			t.Fatal("Invalid cron spec")
  1758  		}
  1759  	}
  1760  }
  1761  
  1762  func TestPeriodicConfig_ValidCron(t *testing.T) {
  1763  	specs := []string{"0 0 29 2 *", "@hourly", "0 0-15 * * *"}
  1764  	for _, spec := range specs {
  1765  		p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec}
  1766  		p.Canonicalize()
  1767  		if err := p.Validate(); err != nil {
  1768  			t.Fatal("Passed valid cron")
  1769  		}
  1770  	}
  1771  }
  1772  
  1773  func TestPeriodicConfig_NextCron(t *testing.T) {
  1774  	from := time.Date(2009, time.November, 10, 23, 22, 30, 0, time.UTC)
  1775  	specs := []string{"0 0 29 2 * 1980", "*/5 * * * *"}
  1776  	expected := []time.Time{time.Time{}, time.Date(2009, time.November, 10, 23, 25, 0, 0, time.UTC)}
  1777  	for i, spec := range specs {
  1778  		p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec}
  1779  		p.Canonicalize()
  1780  		n := p.Next(from)
  1781  		if expected[i] != n {
  1782  			t.Fatalf("Next(%v) returned %v; want %v", from, n, expected[i])
  1783  		}
  1784  	}
  1785  }
  1786  
  1787  func TestPeriodicConfig_ValidTimeZone(t *testing.T) {
  1788  	zones := []string{"Africa/Abidjan", "America/Chicago", "Europe/Minsk", "UTC"}
  1789  	for _, zone := range zones {
  1790  		p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: "0 0 29 2 * 1980", TimeZone: zone}
  1791  		p.Canonicalize()
  1792  		if err := p.Validate(); err != nil {
  1793  			t.Fatalf("Valid tz errored: %v", err)
  1794  		}
  1795  	}
  1796  }
  1797  
  1798  func TestPeriodicConfig_DST(t *testing.T) {
  1799  	// On Sun, Mar 12, 2:00 am 2017: +1 hour UTC
  1800  	p := &PeriodicConfig{
  1801  		Enabled:  true,
  1802  		SpecType: PeriodicSpecCron,
  1803  		Spec:     "0 2 11-12 3 * 2017",
  1804  		TimeZone: "America/Los_Angeles",
  1805  	}
  1806  	p.Canonicalize()
  1807  
  1808  	t1 := time.Date(2017, time.March, 11, 1, 0, 0, 0, p.location)
  1809  	t2 := time.Date(2017, time.March, 12, 1, 0, 0, 0, p.location)
  1810  
  1811  	// E1 is an 8 hour adjustment, E2 is a 7 hour adjustment
  1812  	e1 := time.Date(2017, time.March, 11, 10, 0, 0, 0, time.UTC)
  1813  	e2 := time.Date(2017, time.March, 12, 9, 0, 0, 0, time.UTC)
  1814  
  1815  	n1 := p.Next(t1).UTC()
  1816  	n2 := p.Next(t2).UTC()
  1817  
  1818  	if !reflect.DeepEqual(e1, n1) {
  1819  		t.Fatalf("Got %v; want %v", n1, e1)
  1820  	}
  1821  	if !reflect.DeepEqual(e2, n2) {
  1822  		t.Fatalf("Got %v; want %v", n1, e1)
  1823  	}
  1824  }
  1825  
  1826  func TestRestartPolicy_Validate(t *testing.T) {
  1827  	// Policy with acceptable restart options passes
  1828  	p := &RestartPolicy{
  1829  		Mode:     RestartPolicyModeFail,
  1830  		Attempts: 0,
  1831  		Interval: 5 * time.Second,
  1832  	}
  1833  	if err := p.Validate(); err != nil {
  1834  		t.Fatalf("err: %v", err)
  1835  	}
  1836  
  1837  	// Policy with ambiguous restart options fails
  1838  	p = &RestartPolicy{
  1839  		Mode:     RestartPolicyModeDelay,
  1840  		Attempts: 0,
  1841  		Interval: 5 * time.Second,
  1842  	}
  1843  	if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "ambiguous") {
  1844  		t.Fatalf("expect ambiguity error, got: %v", err)
  1845  	}
  1846  
  1847  	// Bad policy mode fails
  1848  	p = &RestartPolicy{
  1849  		Mode:     "nope",
  1850  		Attempts: 1,
  1851  		Interval: 5 * time.Second,
  1852  	}
  1853  	if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "mode") {
  1854  		t.Fatalf("expect mode error, got: %v", err)
  1855  	}
  1856  
  1857  	// Fails when attempts*delay does not fit inside interval
  1858  	p = &RestartPolicy{
  1859  		Mode:     RestartPolicyModeDelay,
  1860  		Attempts: 3,
  1861  		Delay:    5 * time.Second,
  1862  		Interval: 5 * time.Second,
  1863  	}
  1864  	if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "can't restart") {
  1865  		t.Fatalf("expect restart interval error, got: %v", err)
  1866  	}
  1867  
  1868  	// Fails when interval is to small
  1869  	p = &RestartPolicy{
  1870  		Mode:     RestartPolicyModeDelay,
  1871  		Attempts: 3,
  1872  		Delay:    5 * time.Second,
  1873  		Interval: 2 * time.Second,
  1874  	}
  1875  	if err := p.Validate(); err == nil || !strings.Contains(err.Error(), "Interval can not be less than") {
  1876  		t.Fatalf("expect interval too small error, got: %v", err)
  1877  	}
  1878  }
  1879  
  1880  func TestAllocation_Index(t *testing.T) {
  1881  	a1 := Allocation{
  1882  		Name:      "example.cache[1]",
  1883  		TaskGroup: "cache",
  1884  		JobID:     "example",
  1885  		Job: &Job{
  1886  			ID:         "example",
  1887  			TaskGroups: []*TaskGroup{{Name: "cache"}}},
  1888  	}
  1889  	e1 := uint(1)
  1890  	a2 := a1.Copy()
  1891  	a2.Name = "example.cache[713127]"
  1892  	e2 := uint(713127)
  1893  
  1894  	if a1.Index() != e1 || a2.Index() != e2 {
  1895  		t.Fatalf("Got %d and %d", a1.Index(), a2.Index())
  1896  	}
  1897  }
  1898  
  1899  func TestTaskArtifact_Validate_Source(t *testing.T) {
  1900  	valid := &TaskArtifact{GetterSource: "google.com"}
  1901  	if err := valid.Validate(); err != nil {
  1902  		t.Fatalf("unexpected error: %v", err)
  1903  	}
  1904  }
  1905  
  1906  func TestTaskArtifact_Validate_Dest(t *testing.T) {
  1907  	valid := &TaskArtifact{GetterSource: "google.com"}
  1908  	if err := valid.Validate(); err != nil {
  1909  		t.Fatalf("unexpected error: %v", err)
  1910  	}
  1911  
  1912  	valid.RelativeDest = "local/"
  1913  	if err := valid.Validate(); err != nil {
  1914  		t.Fatalf("unexpected error: %v", err)
  1915  	}
  1916  
  1917  	valid.RelativeDest = "local/.."
  1918  	if err := valid.Validate(); err != nil {
  1919  		t.Fatalf("unexpected error: %v", err)
  1920  	}
  1921  
  1922  	valid.RelativeDest = "local/../../.."
  1923  	if err := valid.Validate(); err == nil {
  1924  		t.Fatalf("expected error: %v", err)
  1925  	}
  1926  }
  1927  
  1928  func TestAllocation_ShouldMigrate(t *testing.T) {
  1929  	alloc := Allocation{
  1930  		TaskGroup: "foo",
  1931  		Job: &Job{
  1932  			TaskGroups: []*TaskGroup{
  1933  				{
  1934  					Name: "foo",
  1935  					EphemeralDisk: &EphemeralDisk{
  1936  						Migrate: true,
  1937  						Sticky:  true,
  1938  					},
  1939  				},
  1940  			},
  1941  		},
  1942  	}
  1943  
  1944  	if !alloc.ShouldMigrate() {
  1945  		t.Fatalf("bad: %v", alloc)
  1946  	}
  1947  
  1948  	alloc1 := Allocation{
  1949  		TaskGroup: "foo",
  1950  		Job: &Job{
  1951  			TaskGroups: []*TaskGroup{
  1952  				{
  1953  					Name:          "foo",
  1954  					EphemeralDisk: &EphemeralDisk{},
  1955  				},
  1956  			},
  1957  		},
  1958  	}
  1959  
  1960  	if alloc1.ShouldMigrate() {
  1961  		t.Fatalf("bad: %v", alloc)
  1962  	}
  1963  
  1964  	alloc2 := Allocation{
  1965  		TaskGroup: "foo",
  1966  		Job: &Job{
  1967  			TaskGroups: []*TaskGroup{
  1968  				{
  1969  					Name: "foo",
  1970  					EphemeralDisk: &EphemeralDisk{
  1971  						Sticky:  false,
  1972  						Migrate: true,
  1973  					},
  1974  				},
  1975  			},
  1976  		},
  1977  	}
  1978  
  1979  	if alloc2.ShouldMigrate() {
  1980  		t.Fatalf("bad: %v", alloc)
  1981  	}
  1982  
  1983  	alloc3 := Allocation{
  1984  		TaskGroup: "foo",
  1985  		Job: &Job{
  1986  			TaskGroups: []*TaskGroup{
  1987  				{
  1988  					Name: "foo",
  1989  				},
  1990  			},
  1991  		},
  1992  	}
  1993  
  1994  	if alloc3.ShouldMigrate() {
  1995  		t.Fatalf("bad: %v", alloc)
  1996  	}
  1997  }
  1998  
  1999  func TestTaskArtifact_Validate_Checksum(t *testing.T) {
  2000  	cases := []struct {
  2001  		Input *TaskArtifact
  2002  		Err   bool
  2003  	}{
  2004  		{
  2005  			&TaskArtifact{
  2006  				GetterSource: "foo.com",
  2007  				GetterOptions: map[string]string{
  2008  					"checksum": "no-type",
  2009  				},
  2010  			},
  2011  			true,
  2012  		},
  2013  		{
  2014  			&TaskArtifact{
  2015  				GetterSource: "foo.com",
  2016  				GetterOptions: map[string]string{
  2017  					"checksum": "md5:toosmall",
  2018  				},
  2019  			},
  2020  			true,
  2021  		},
  2022  		{
  2023  			&TaskArtifact{
  2024  				GetterSource: "foo.com",
  2025  				GetterOptions: map[string]string{
  2026  					"checksum": "invalid:type",
  2027  				},
  2028  			},
  2029  			true,
  2030  		},
  2031  	}
  2032  
  2033  	for i, tc := range cases {
  2034  		err := tc.Input.Validate()
  2035  		if (err != nil) != tc.Err {
  2036  			t.Fatalf("case %d: %v", i, err)
  2037  			continue
  2038  		}
  2039  	}
  2040  }
  2041  
  2042  func TestAllocation_Terminated(t *testing.T) {
  2043  	type desiredState struct {
  2044  		ClientStatus  string
  2045  		DesiredStatus string
  2046  		Terminated    bool
  2047  	}
  2048  
  2049  	harness := []desiredState{
  2050  		{
  2051  			ClientStatus:  AllocClientStatusPending,
  2052  			DesiredStatus: AllocDesiredStatusStop,
  2053  			Terminated:    false,
  2054  		},
  2055  		{
  2056  			ClientStatus:  AllocClientStatusRunning,
  2057  			DesiredStatus: AllocDesiredStatusStop,
  2058  			Terminated:    false,
  2059  		},
  2060  		{
  2061  			ClientStatus:  AllocClientStatusFailed,
  2062  			DesiredStatus: AllocDesiredStatusStop,
  2063  			Terminated:    true,
  2064  		},
  2065  		{
  2066  			ClientStatus:  AllocClientStatusFailed,
  2067  			DesiredStatus: AllocDesiredStatusRun,
  2068  			Terminated:    true,
  2069  		},
  2070  	}
  2071  
  2072  	for _, state := range harness {
  2073  		alloc := Allocation{}
  2074  		alloc.DesiredStatus = state.DesiredStatus
  2075  		alloc.ClientStatus = state.ClientStatus
  2076  		if alloc.Terminated() != state.Terminated {
  2077  			t.Fatalf("expected: %v, actual: %v", state.Terminated, alloc.Terminated())
  2078  		}
  2079  	}
  2080  }
  2081  
  2082  func TestVault_Validate(t *testing.T) {
  2083  	v := &Vault{
  2084  		Env:        true,
  2085  		ChangeMode: VaultChangeModeNoop,
  2086  	}
  2087  
  2088  	if err := v.Validate(); err == nil || !strings.Contains(err.Error(), "Policy list") {
  2089  		t.Fatalf("Expected policy list empty error")
  2090  	}
  2091  
  2092  	v.Policies = []string{"foo", "root"}
  2093  	v.ChangeMode = VaultChangeModeSignal
  2094  
  2095  	err := v.Validate()
  2096  	if err == nil {
  2097  		t.Fatalf("Expected validation errors")
  2098  	}
  2099  
  2100  	if !strings.Contains(err.Error(), "Signal must") {
  2101  		t.Fatalf("Expected signal empty error")
  2102  	}
  2103  	if !strings.Contains(err.Error(), "root") {
  2104  		t.Fatalf("Expected root error")
  2105  	}
  2106  }
  2107  
  2108  func TestParameterizedJobConfig_Validate(t *testing.T) {
  2109  	d := &ParameterizedJobConfig{
  2110  		Payload: "foo",
  2111  	}
  2112  
  2113  	if err := d.Validate(); err == nil || !strings.Contains(err.Error(), "payload") {
  2114  		t.Fatalf("Expected unknown payload requirement: %v", err)
  2115  	}
  2116  
  2117  	d.Payload = DispatchPayloadOptional
  2118  	d.MetaOptional = []string{"foo", "bar"}
  2119  	d.MetaRequired = []string{"bar", "baz"}
  2120  
  2121  	if err := d.Validate(); err == nil || !strings.Contains(err.Error(), "disjoint") {
  2122  		t.Fatalf("Expected meta not being disjoint error: %v", err)
  2123  	}
  2124  }
  2125  
  2126  func TestParameterizedJobConfig_Validate_NonBatch(t *testing.T) {
  2127  	job := testJob()
  2128  	job.ParameterizedJob = &ParameterizedJobConfig{
  2129  		Payload: DispatchPayloadOptional,
  2130  	}
  2131  	job.Type = JobTypeSystem
  2132  
  2133  	if err := job.Validate(); err == nil || !strings.Contains(err.Error(), "only be used with") {
  2134  		t.Fatalf("Expected bad scheduler tpye: %v", err)
  2135  	}
  2136  }
  2137  
  2138  func TestParameterizedJobConfig_Canonicalize(t *testing.T) {
  2139  	d := &ParameterizedJobConfig{}
  2140  	d.Canonicalize()
  2141  	if d.Payload != DispatchPayloadOptional {
  2142  		t.Fatalf("Canonicalize failed")
  2143  	}
  2144  }
  2145  
  2146  func TestDispatchPayloadConfig_Validate(t *testing.T) {
  2147  	d := &DispatchPayloadConfig{
  2148  		File: "foo",
  2149  	}
  2150  
  2151  	// task/local/haha
  2152  	if err := d.Validate(); err != nil {
  2153  		t.Fatalf("bad: %v", err)
  2154  	}
  2155  
  2156  	// task/haha
  2157  	d.File = "../haha"
  2158  	if err := d.Validate(); err != nil {
  2159  		t.Fatalf("bad: %v", err)
  2160  	}
  2161  
  2162  	// ../haha
  2163  	d.File = "../../../haha"
  2164  	if err := d.Validate(); err == nil {
  2165  		t.Fatalf("bad: %v", err)
  2166  	}
  2167  }
  2168  
  2169  func TestIsRecoverable(t *testing.T) {
  2170  	if IsRecoverable(nil) {
  2171  		t.Errorf("nil should not be recoverable")
  2172  	}
  2173  	if IsRecoverable(NewRecoverableError(nil, true)) {
  2174  		t.Errorf("NewRecoverableError(nil, true) should not be recoverable")
  2175  	}
  2176  	if IsRecoverable(fmt.Errorf("i promise im recoverable")) {
  2177  		t.Errorf("Custom errors should not be recoverable")
  2178  	}
  2179  	if IsRecoverable(NewRecoverableError(fmt.Errorf(""), false)) {
  2180  		t.Errorf("Explicitly unrecoverable errors should not be recoverable")
  2181  	}
  2182  	if !IsRecoverable(NewRecoverableError(fmt.Errorf(""), true)) {
  2183  		t.Errorf("Explicitly recoverable errors *should* be recoverable")
  2184  	}
  2185  }